package httpapi

// Integration tests for the Phase 2 mutation surface: entity create/patch
// with optimistic concurrency, idempotency, lifecycle-transition validation,
// and the audit/event side effects. Guarded by OIKOS_TEST_DATABASE_URL.

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
)

// do issues a JSON request and returns the recorder + decoded body.
func do(t *testing.T, h http.Handler, method, path string, body any, headers map[string]string) (*httptest.ResponseRecorder, map[string]any) {
	t.Helper()
	var rdr *bytes.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		rdr = bytes.NewReader(b)
	} else {
		rdr = bytes.NewReader(nil)
	}
	req := httptest.NewRequest(method, path, rdr)
	req.Header.Set("Content-Type", "application/json")
	applyHeaders(req, headers)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	var decoded map[string]any
	json.Unmarshal(rec.Body.Bytes(), &decoded)
	return rec, decoded
}

func TestEntityCreateAndPatch(t *testing.T) {
	h := newTestHandler(t, devConfig())

	// ── create ──────────────────────────────────────────────────
	rec, body := do(t, h, "POST", "/api/v1/entities", map[string]any{
		"slug": "service:test-widget",
		"type": "service",
		"name": "test-widget",
		"attributes": map[string]any{"port": 9999},
	}, nil)
	if rec.Code != 201 {
		t.Fatalf("create status %d: %v", rec.Code, body)
	}
	if body["slug"] != "service:test-widget" {
		t.Fatalf("created slug = %v", body["slug"])
	}
	// default lifecycle state applied
	if body["state"] != "active" {
		t.Errorf("default state = %v, want active", body["state"])
	}
	etag := rec.Header().Get("ETag")
	if etag == "" {
		t.Error("missing ETag on create")
	}
	version := int(body["version"].(float64))

	// ── duplicate slug → 409 ────────────────────────────────────
	rec, _ = do(t, h, "POST", "/api/v1/entities", map[string]any{
		"slug": "service:test-widget", "type": "service", "name": "dup",
	}, nil)
	if rec.Code != 409 {
		t.Errorf("duplicate slug status = %d, want 409", rec.Code)
	}

	// ── abstract type → 422 ─────────────────────────────────────
	rec, _ = do(t, h, "POST", "/api/v1/entities", map[string]any{
		"slug": "machine:ghost", "type": "machine", "name": "ghost",
	}, nil)
	if rec.Code != 422 {
		t.Errorf("abstract type status = %d, want 422", rec.Code)
	}

	// ── patch without If-Match → 400 ────────────────────────────
	rec, _ = do(t, h, "PATCH", "/api/v1/entities/service:test-widget",
		map[string]any{"name": "renamed"}, nil)
	if rec.Code != 400 {
		t.Errorf("patch w/o If-Match = %d, want 400", rec.Code)
	}

	// ── patch with stale If-Match → 409 ─────────────────────────
	rec, _ = do(t, h, "PATCH", "/api/v1/entities/service:test-widget",
		map[string]any{"name": "renamed"}, map[string]string{"If-Match": `"999"`})
	if rec.Code != 409 {
		t.Errorf("stale If-Match = %d, want 409", rec.Code)
	}

	// ── valid attribute patch → 200, version bumps ──────────────
	rec, body = do(t, h, "PATCH", "/api/v1/entities/service:test-widget",
		map[string]any{"name": "renamed"}, map[string]string{"If-Match": itoaQ(version)})
	if rec.Code != 200 {
		t.Fatalf("patch status %d: %v", rec.Code, body)
	}
	if body["name"] != "renamed" || int(body["version"].(float64)) != version+1 {
		t.Errorf("patch result: name=%v version=%v", body["name"], body["version"])
	}
	version++

	// ── valid lifecycle transition active→deprecated → 200 ──────
	// (this is the regression guard for the transitions-parsing 500 bug)
	rec, body = do(t, h, "PATCH", "/api/v1/entities/service:test-widget",
		map[string]any{"state": "deprecated"}, map[string]string{"If-Match": itoaQ(version)})
	if rec.Code != 200 {
		t.Fatalf("valid transition status %d: %v", rec.Code, body)
	}
	if body["state"] != "deprecated" {
		t.Errorf("state = %v, want deprecated", body["state"])
	}
	version++

	// ── invalid lifecycle transition deprecated→provisioning → 409 ──
	rec, _ = do(t, h, "PATCH", "/api/v1/entities/service:test-widget",
		map[string]any{"state": "provisioning"}, map[string]string{"If-Match": itoaQ(version)})
	if rec.Code != 409 {
		t.Errorf("invalid transition status = %d, want 409", rec.Code)
	}
}

func TestEntityCreateIdempotency(t *testing.T) {
	h := newTestHandler(t, devConfig())
	payload := map[string]any{"slug": "service:idem", "type": "service", "name": "idem"}
	key := map[string]string{"Idempotency-Key": "abc-123"}

	rec1, body1 := do(t, h, "POST", "/api/v1/entities", payload, key)
	if rec1.Code != 201 {
		t.Fatalf("first create %d: %v", rec1.Code, body1)
	}
	// replay same key + body → same response, not a duplicate-slug 409
	rec2, body2 := do(t, h, "POST", "/api/v1/entities", payload, key)
	if rec2.Code != 201 {
		t.Fatalf("idempotent replay = %d, want 201: %v", rec2.Code, body2)
	}
	if body1["id"] != body2["id"] {
		t.Errorf("replay returned different entity: %v vs %v", body1["id"], body2["id"])
	}

	// same key, different body → 409 conflict
	rec3, _ := do(t, h, "POST", "/api/v1/entities",
		map[string]any{"slug": "service:idem2", "type": "service", "name": "idem2"}, key)
	if rec3.Code != 409 {
		t.Errorf("key reuse w/ different body = %d, want 409", rec3.Code)
	}
}

func TestMutationEmitsEventAndAudit(t *testing.T) {
	h := newTestHandler(t, devConfig())

	rec, _ := do(t, h, "POST", "/api/v1/entities",
		map[string]any{"slug": "service:evt", "type": "service", "name": "evt"}, nil)
	if rec.Code != 201 {
		t.Fatalf("create failed: %d", rec.Code)
	}

	// event stream recorded the creation
	_, body := do(t, h, "GET", "/api/v1/events?type=entity.created", nil, nil)
	items, _ := body["items"].([]any)
	if len(items) == 0 {
		t.Fatal("no entity.created event recorded")
	}

	// audit trail recorded the create (operator-visible)
	_, abody := do(t, h, "GET", "/api/v1/audit?action=create", nil, nil)
	aitems, _ := abody["items"].([]any)
	if len(aitems) == 0 {
		t.Fatal("no create audit entry recorded")
	}
}

// itoaQ formats an int as a quoted ETag value.
func itoaQ(v int) string {
	b, _ := json.Marshal(v)
	return `"` + string(b) + `"`
}
