package exec

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

func TestReviewHandlerUsesTypedRequestsAndAuthorizedServer(t *testing.T) {
	var method, path string
	var addBody map[string]any
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		method, path = r.Method, r.URL.Path
		switch {
		case r.Method == http.MethodPost && path == "/api/v1/workspaces/workspace-1/agents/server-agent-1/runtime/resolve-channel":
			var body map[string]string
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			require.Equal(t, "#agentteam-in-agentteam:thread-1", body["target"])
			_, _ = io.WriteString(w, `{"scopeId":"thread-scope-1"}`)
		case r.Method == http.MethodGet:
			_, _ = io.WriteString(w, `{"contexts":[{"id":"rcx-1","kind":"git_repo","rootPath":"/repo"}]}`)
		case r.Method == http.MethodPut:
			require.NoError(t, json.NewDecoder(r.Body).Decode(&addBody))
			_, _ = io.WriteString(w, `{"context":{"id":"rcx-2","kind":"git_repo","rootPath":"/repo"}}`)
		case r.Method == http.MethodDelete:
			w.WriteHeader(http.StatusNoContent)
		default:
			http.NotFound(w, r)
		}
	}))
	t.Cleanup(server.Close)
	authorized := runtimeAuthorization{
		Server: &apiclient.Client{
			ServerURL: server.URL, RouteWorkspaceID: "workspace-1",
			AgentID: "server-agent-1", AuthToken: "server-token", HTTPClient: server.Client(),
		},
	}
	handler := reviewHandler{}

	list, err := handler.list(context.Background(), actionScopedJSON[reviewListRequest]{
		Authorized: authorized,
		Body:       reviewListRequest{Thread: "#agentteam-in-agentteam:thread-1"},
	})
	require.NoError(t, err)
	require.Equal(t, []reviewContext{{ID: "rcx-1", Kind: "git_repo", RootPath: "/repo"}}, list.Data.Contexts)

	add, err := handler.add(context.Background(), actionScopedJSON[reviewAddInput]{
		Authorized: authorized,
		Body: reviewAddInput{
			Path: "/repo", Thread: "#agentteam-in-agentteam:thread-1",
			Name: "Repository", PRURL: "https://example.com/pr/1",
		},
	})
	require.NoError(t, err)
	require.Equal(t, "rcx-2", add.Data.Context.ID)
	require.Equal(t, "Repository", addBody["displayName"])
	require.Equal(t, "https://example.com/pr/1", addBody["prUrl"])
	require.NotContains(t, addBody, "name")

	deleted, err := handler.delete(context.Background(), actionScopedJSON[reviewDeleteRequest]{
		Authorized: authorized,
		Body:       reviewDeleteRequest{ContextID: "rcx-2"},
	})
	require.NoError(t, err)
	require.Equal(t, reviewDeleteResult{ContextID: "rcx-2"}, deleted.Data)
	require.Equal(t, http.MethodDelete, method)
	require.Contains(t, path, "/review-contexts/rcx-2")
}

func TestReviewHandlerRejectsMissingThreadWithoutCallingServer(t *testing.T) {
	handler := reviewHandler{}
	_, err := handler.list(context.Background(), actionScopedJSON[reviewListRequest]{
		Authorized: runtimeAuthorization{Server: &apiclient.Client{}},
	})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)
}

func TestReviewHandlerSupportsTaskScopeAndRejectsAmbiguousScope(t *testing.T) {
	var gotPath string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotPath = r.URL.Path
		if r.Method == http.MethodGet {
			_, _ = io.WriteString(w, `{"contexts":[]}`)
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()
	authorized := runtimeAuthorization{Server: &apiclient.Client{ServerURL: server.URL, HTTPClient: server.Client(), RouteWorkspaceID: "ws", AgentID: "agent", AuthToken: "token"}}
	_, err := (reviewHandler{}).list(context.Background(), actionScopedJSON[reviewListRequest]{Authorized: authorized, Body: reviewListRequest{TaskScope: &reviewTaskScope{Kind: "task", ScopeID: "tsk_123"}}})
	require.NoError(t, err)
	require.Equal(t, "/api/v1/workspaces/ws/agents/agent/runtime/tasks/tsk_123/review-contexts", gotPath)
	_, err = (reviewHandler{}).list(context.Background(), actionScopedJSON[reviewListRequest]{Authorized: authorized, Body: reviewListRequest{Thread: "#channel:thread", TaskScope: &reviewTaskScope{Kind: "task", ScopeID: "tsk_123"}}})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)
}

func TestReviewHandlerAddsTaskScopeWithoutThreadResolution(t *testing.T) {
	var gotPath string
	var gotBody map[string]any
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotPath = r.URL.Path
		if r.Method != http.MethodPut {
			http.NotFound(w, r)
			return
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&gotBody))
		_, _ = io.WriteString(w, `{"context":{"id":"rcx-task","taskScopeId":"tsk_123","rootPath":"/repo"}}`)
	}))
	defer server.Close()
	authorized := runtimeAuthorization{Server: &apiclient.Client{ServerURL: server.URL, HTTPClient: server.Client(), RouteWorkspaceID: "ws", AgentID: "agent", AuthToken: "token"}}
	result, err := (reviewHandler{}).add(context.Background(), actionScopedJSON[reviewAddInput]{
		Authorized: authorized,
		Body: reviewAddInput{
			Path:      "/repo",
			TaskScope: &reviewTaskScope{Kind: "task", ScopeID: "tsk_123"},
		},
	})
	require.NoError(t, err)
	require.Equal(t, "rcx-task", result.Data.Context.ID)
	require.Equal(t, "/api/v1/workspaces/ws/agents/agent/runtime/tasks/tsk_123/review-context", gotPath)
	require.Equal(t, map[string]any{
		"kind": "git_repo", "rootPath": "/repo", "taskScope": map[string]any{"kind": "task", "scopeId": "tsk_123"},
		"baseRef": "", "headRef": "", "displayName": "", "prUrl": "",
	}, gotBody)
}

func TestReviewHandlerRequiresBinderCanonicalThread(t *testing.T) {
	_, err := reviewThreadTarget("abc123ef")
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)

	target, err := reviewThreadTarget("#agentteam-in-agentteam:abc123ef")
	require.NoError(t, err)
	require.Equal(t, "#agentteam-in-agentteam:abc123ef", target)
}

func TestReviewSchemasUseFieldsAndConstraints(t *testing.T) {
	add, err := describe(reviewAddSpec)
	require.NoError(t, err)
	for _, field := range []string{`"thread"`, `"taskScope"`, `"name"`, `"prUrl"`} {
		require.Contains(t, string(add.InputSchema), field)
	}
	for _, legacy := range []string{"thread_id", "display_name", "pr_url"} {
		require.NotContains(t, string(add.InputSchema), legacy)
	}
	addSchema := mustCompile(t, add.InputSchema)
	require.NoError(t, validateInstance(addSchema, json.RawMessage(`{"path":"/repo","thread":"#channel:thread"}`)))
	require.NoError(t, validateInstance(addSchema, json.RawMessage(`{"path":"/repo","taskScope":{"kind":"task","scopeId":"tsk_123"}}`)))
	require.Error(t, validateInstance(addSchema, json.RawMessage(`{"path":"   ","thread":"#channel:thread"}`)))

	deleteDescriptor, err := describe(reviewDeleteSpec)
	require.NoError(t, err)
	require.Contains(t, string(deleteDescriptor.InputSchema), `"contextId"`)
	require.Contains(t, string(deleteDescriptor.OutputSchema), `"contextId"`)
	require.NotContains(t, string(deleteDescriptor.InputSchema), `"context_id"`)
	deleteSchema := mustCompile(t, deleteDescriptor.InputSchema)
	require.Error(t, validateInstance(deleteSchema, json.RawMessage(`{"contextId":"   "}`)))
}
