package project

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

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
)

// fakeResolver 把固定 token "tok-<uid>" 解码为 uuid。
type fakeResolver struct{ valid map[string]uuid.UUID }

func (f *fakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) {
	uid, ok := f.valid[token]
	return uid, ok, nil
}

// fakeAdmin 始终返回 false（owner 路径足够覆盖大多数测试）。
type fakeAdmin struct{}

func (fakeAdmin) IsAdmin(_ context.Context, _ uuid.UUID) (bool, error) { return false, nil }

func mountFullHandler(t *testing.T, ownerToken string, ownerID uuid.UUID) (*fakeRepo, http.Handler) {
	t.Helper()
	repo := newFakeRepo()
	pSvc := NewProjectService(repo, nil)
	gate := NewGateRunner(GateDeps{Repo: repo, Workspace: nil}, nil)
	rSvc := NewRequirementService(repo, nil, nil, gate)
	iSvc := NewIssueService(repo, nil)
	artSvc := NewArtifactService(repo, nil)
	resolver := &fakeResolver{valid: map[string]uuid.UUID{ownerToken: ownerID}}
	h := NewHandler(pSvc, rSvc, iSvc, artSvc, resolver, fakeAdmin{})
	r := chi.NewRouter()
	r.Use(middleware.RequestID)
	h.Mount(r)
	return repo, r
}

func doJSON(t *testing.T, h http.Handler, method, path, token string, body any) *http.Response {
	t.Helper()
	var b []byte
	if body != nil {
		b, _ = json.Marshal(body)
	}
	req := httptest.NewRequest(method, path, bytes.NewReader(b))
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	return rec.Result()
}

// checkStatus calls doJSON, asserts the HTTP status, and closes the response body.
func checkStatus(t *testing.T, h http.Handler, method, path, token string, body any, wantStatus int) {
	t.Helper()
	resp := doJSON(t, h, method, path, token, body)
	t.Cleanup(func() { _ = resp.Body.Close() })
	require.Equal(t, wantStatus, resp.StatusCode)
}

func TestHandler_FullClosedLoop(t *testing.T) {
	owner := uuid.New()
	tok := "tok-1"
	_, h := mountFullHandler(t, tok, owner)

	// 1. create project
	checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "Demo"}, http.StatusCreated)

	// 2. create 2 requirements
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R1"}, http.StatusCreated)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R2"}, http.StatusCreated)

	// 3. phase change
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/phase", tok, map[string]any{"phase": "auditing"}, http.StatusOK)

	// 4. create issue attached + freelance
	checkStatus(t, h, "POST", "/api/v1/projects/demo/issues", tok, map[string]any{"title": "I1", "requirement_number": 1}, http.StatusCreated)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/issues", tok, map[string]any{"title": "Free"}, http.StatusCreated)

	// 5. close requirement → phase change must fail with 409 / requirement_closed
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/close", tok, nil, http.StatusNoContent)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/phase", tok, map[string]any{"phase": "done"}, http.StatusConflict)

	// 6. archive project → all writes blocked
	checkStatus(t, h, "POST", "/api/v1/projects/demo/archive", tok, nil, http.StatusNoContent)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "X"}, http.StatusConflict)
}

func TestHandler_Unauthenticated_Returns401(t *testing.T) {
	owner := uuid.New()
	_, h := mountFullHandler(t, "tok", owner)
	resp := doJSON(t, h, "GET", "/api/v1/projects/", "", nil)
	t.Cleanup(func() { _ = resp.Body.Close() })
	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}

func TestHandler_SlugTaken_Returns409(t *testing.T) {
	owner := uuid.New()
	tok := "tok"
	_, h := mountFullHandler(t, tok, owner)
	checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "A"}, http.StatusCreated)
	resp := doJSON(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "B"})
	t.Cleanup(func() { _ = resp.Body.Close() })
	require.Equal(t, http.StatusConflict, resp.StatusCode)
	var body map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	require.Equal(t, "project_slug_taken", body["code"])
}

// decodeBodyMap 解析 200 响应里的 JSON 对象。
func decodeBodyMap(t *testing.T, resp *http.Response) map[string]any {
	t.Helper()
	t.Cleanup(func() { _ = resp.Body.Close() })
	var body map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	return body
}

// TestHandler_Issue_PATCH_WorkspaceID_TriState 验证 PATCH /issues/{n} 的
// workspace_id 三态：缺失=不变 / null=解绑 / UUID 字符串=关联 / 非法=400。
func TestHandler_Issue_PATCH_WorkspaceID_TriState(t *testing.T) {
	owner := uuid.New()
	tok := "tok"
	_, h := mountFullHandler(t, tok, owner)
	checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "Demo"}, http.StatusCreated)
	wsID := uuid.New()
	resp := doJSON(t, h, "POST", "/api/v1/projects/demo/issues", tok, map[string]any{"title": "I", "workspace_id": wsID.String()})
	t.Cleanup(func() { _ = resp.Body.Close() })
	require.Equal(t, http.StatusCreated, resp.StatusCode)
	createBody := decodeBodyMap(t, resp)
	require.Equal(t, wsID.String(), createBody["workspace_id"])

	// 1) 缺失字段：不变
	resp2 := doJSON(t, h, "PATCH", "/api/v1/projects/demo/issues/1", tok, map[string]any{"title": "I-new"})
	require.Equal(t, http.StatusOK, resp2.StatusCode)
	body2 := decodeBodyMap(t, resp2)
	require.Equal(t, wsID.String(), body2["workspace_id"])

	// 2) UUID 字符串：关联到新 workspace
	wsB := uuid.New()
	resp3 := doJSON(t, h, "PATCH", "/api/v1/projects/demo/issues/1", tok, map[string]any{"workspace_id": wsB.String()})
	require.Equal(t, http.StatusOK, resp3.StatusCode)
	body3 := decodeBodyMap(t, resp3)
	require.Equal(t, wsB.String(), body3["workspace_id"])

	// 3) 显式 null：解绑（用 raw JSON 才能传 null，不能用 map[string]any{"workspace_id": nil}，
	//    因为 map nil 也会被序列化成 null，这里直接拼字符串保证准确）
	rawNull := bytes.NewBufferString(`{"workspace_id": null}`)
	req := httptest.NewRequest("PATCH", "/api/v1/projects/demo/issues/1", rawNull)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+tok)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	resp4 := rec.Result()
	require.Equal(t, http.StatusOK, resp4.StatusCode)
	body4 := decodeBodyMap(t, resp4)
	require.Nil(t, body4["workspace_id"])

	// 4) 非法字符串：400
	resp5 := doJSON(t, h, "PATCH", "/api/v1/projects/demo/issues/1", tok, map[string]any{"workspace_id": "not-a-uuid"})
	t.Cleanup(func() { _ = resp5.Body.Close() })
	require.Equal(t, http.StatusBadRequest, resp5.StatusCode)
}

// TestHandler_Requirement_PATCH_WorkspaceID_TriState 验证 PATCH /requirements/{n}
// 的 workspace_id 三态。
func TestHandler_Requirement_PATCH_WorkspaceID_TriState(t *testing.T) {
	owner := uuid.New()
	tok := "tok"
	_, h := mountFullHandler(t, tok, owner)
	checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "Demo"}, http.StatusCreated)
	wsID := uuid.New()
	resp := doJSON(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R", "workspace_id": wsID.String()})
	require.Equal(t, http.StatusCreated, resp.StatusCode)
	createBody := decodeBodyMap(t, resp)
	require.Equal(t, wsID.String(), createBody["workspace_id"])

	// 1) 缺失字段：不变
	resp2 := doJSON(t, h, "PATCH", "/api/v1/projects/demo/requirements/1", tok, map[string]any{"title": "R-new"})
	require.Equal(t, http.StatusOK, resp2.StatusCode)
	body2 := decodeBodyMap(t, resp2)
	require.Equal(t, wsID.String(), body2["workspace_id"])

	// 2) UUID 字符串：关联到新 workspace
	wsB := uuid.New()
	resp3 := doJSON(t, h, "PATCH", "/api/v1/projects/demo/requirements/1", tok, map[string]any{"workspace_id": wsB.String()})
	require.Equal(t, http.StatusOK, resp3.StatusCode)
	body3 := decodeBodyMap(t, resp3)
	require.Equal(t, wsB.String(), body3["workspace_id"])

	// 3) 显式 null：解绑
	rawNull := bytes.NewBufferString(`{"workspace_id": null}`)
	req := httptest.NewRequest("PATCH", "/api/v1/projects/demo/requirements/1", rawNull)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+tok)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	resp4 := rec.Result()
	require.Equal(t, http.StatusOK, resp4.StatusCode)
	body4 := decodeBodyMap(t, resp4)
	require.Nil(t, body4["workspace_id"])

	// 4) 非法字符串：400
	resp5 := doJSON(t, h, "PATCH", "/api/v1/projects/demo/requirements/1", tok, map[string]any{"workspace_id": "not-a-uuid"})
	t.Cleanup(func() { _ = resp5.Body.Close() })
	require.Equal(t, http.StatusBadRequest, resp5.StatusCode)
}

// TestHandler_Artifacts_Routes 验证 artifacts 三条路由：创建 201、list 裸数组 +
// ?phase= 过滤、{phase}/{version} 获取、非法 phase 400。
func TestHandler_Artifacts_Routes(t *testing.T) {
	owner := uuid.New()
	tok := "tok"
	_, h := mountFullHandler(t, tok, owner)
	checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "Demo"}, http.StatusCreated)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R"}, http.StatusCreated)

	// 创建 planning v1（带 source_message_id）+ auditing v1
	resp := doJSON(t, h, "POST", "/api/v1/projects/demo/requirements/1/artifacts", tok,
		map[string]any{"phase": "planning", "content": "# plan", "note": "n1", "source_message_id": 7})
	require.Equal(t, http.StatusCreated, resp.StatusCode)
	created := decodeBodyMap(t, resp)
	require.Equal(t, "planning", created["phase"])
	require.Equal(t, float64(1), created["version"])
	require.Equal(t, float64(7), created["source_message_id"])
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/artifacts", tok,
		map[string]any{"phase": "auditing", "content": "# audit"}, http.StatusCreated)

	// 非法 phase（done 不在 ArtifactPhases）→ 400
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/artifacts", tok,
		map[string]any{"phase": "done", "content": "x"}, http.StatusBadRequest)

	// list 裸数组：全量 2 条；?phase=planning 过滤 1 条
	respList := doJSON(t, h, "GET", "/api/v1/projects/demo/requirements/1/artifacts", tok, nil)
	t.Cleanup(func() { _ = respList.Body.Close() })
	require.Equal(t, http.StatusOK, respList.StatusCode)
	var all []map[string]any
	require.NoError(t, json.NewDecoder(respList.Body).Decode(&all))
	require.Len(t, all, 2)

	respPlanning := doJSON(t, h, "GET", "/api/v1/projects/demo/requirements/1/artifacts?phase=planning", tok, nil)
	t.Cleanup(func() { _ = respPlanning.Body.Close() })
	require.Equal(t, http.StatusOK, respPlanning.StatusCode)
	var planning []map[string]any
	require.NoError(t, json.NewDecoder(respPlanning.Body).Decode(&planning))
	require.Len(t, planning, 1)
	require.Equal(t, "# plan", planning[0]["content"])

	// get by {phase}/{version}
	respGet := doJSON(t, h, "GET", "/api/v1/projects/demo/requirements/1/artifacts/planning/1", tok, nil)
	got := decodeBodyMap(t, respGet)
	require.Equal(t, http.StatusOK, respGet.StatusCode)
	require.Equal(t, "# plan", got["content"])

	// get 非法 phase → 400；不存在版本 → 404
	checkStatus(t, h, "GET", "/api/v1/projects/demo/requirements/1/artifacts/bogus/1", tok, nil, http.StatusBadRequest)
	checkStatus(t, h, "GET", "/api/v1/projects/demo/requirements/1/artifacts/planning/9", tok, nil, http.StatusNotFound)
}

// TestHandler_ApproveArtifactAndGatedPhase 验证 approve 端点回写 verdict，且未满足
// gate 的 phase 流转返回 409 phase_gate_failed，批准后放行 200。
func TestHandler_ApproveArtifactAndGatedPhase(t *testing.T) {
	owner := uuid.New()
	tok := "tok"
	_, h := mountFullHandler(t, tok, owner)
	checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "Demo"}, http.StatusCreated)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R"}, http.StatusCreated)
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/artifacts", tok,
		map[string]any{"phase": "auditing", "content": "# audit"}, http.StatusCreated)

	// 未批准 auditing 产物时流转到 implementing → 409 phase_gate_failed
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/phase", tok,
		map[string]any{"phase": "implementing"}, http.StatusConflict)

	// approve 端点回写 verdict
	respApprove := doJSON(t, h, "POST", "/api/v1/projects/demo/requirements/1/artifacts/auditing/1/approve", tok,
		map[string]any{"verdict": "pass"})
	require.Equal(t, http.StatusOK, respApprove.StatusCode)
	approved := decodeBodyMap(t, respApprove)
	require.Equal(t, "pass", approved["verdict"])

	// 批准后流转放行 → 200
	checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/phase", tok,
		map[string]any{"phase": "implementing"}, http.StatusOK)
}
