package todoist_test

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/binding"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/todoist"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

// wireCase pins one action's upstream wire contract: method, path, query, and
// body semantics, exercised against a TLS loopback double.
type wireCase struct {
	action string
	input  string
	method string
	path   string
	query  string
	// bodyContains asserts substrings of the JSON request body.
	bodyContains []string
	// bodyAbsent asserts keys absent from the request body.
	bodyAbsent []string
}

func TestTodoistWireContractCoversEveryAction(t *testing.T) {
	t.Parallel()

	cases := []wireCase{
		{
			action: todoist.ActionGetCurrentUser, input: `{}`,
			method: http.MethodGet, path: "/user",
		},
		{
			action: todoist.ActionListProjects, input: `{"limit":25,"cursor":"c1"}`,
			method: http.MethodGet, path: "/projects", query: "cursor=c1&limit=25",
		},
		{
			action: todoist.ActionGetProject, input: `{"projectId":"6XG"}`,
			method: http.MethodGet, path: "/projects/6XG",
		},
		{
			action: todoist.ActionCreateProject,
			input:  `{"name":"AgentTeam","parentId":"6XF","workspaceId":"12345"}`,
			method: http.MethodPost, path: "/projects",
			bodyContains: []string{`"name":"AgentTeam"`, `"parent_id":"6XF"`, `"workspace_id":12345`},
			bodyAbsent:   []string{"is_favorite"},
		},
		{
			action: todoist.ActionCreateProject,
			input:  `{"name":"AgentTeam","parentId":null}`,
			method: http.MethodPost, path: "/projects",
			bodyContains: []string{`"parent_id":null`},
		},
		{
			action: todoist.ActionCreateProject,
			input:  `{"name":"AgentTeam"}`,
			method: http.MethodPost, path: "/projects",
			bodyContains: []string{`"name":"AgentTeam"`},
			bodyAbsent:   []string{"parent_id", "workspace_id"},
		},
		{
			action: todoist.ActionUpdateProject,
			input:  `{"projectId":"6XG","name":null,"isFavorite":false}`,
			method: http.MethodPost, path: "/projects/6XG",
			bodyContains: []string{`"name":null`, `"is_favorite":false`},
			bodyAbsent:   []string{"description", "folder_id"},
		},
		{
			action: todoist.ActionListSections, input: `{"projectId":"6XG","limit":10}`,
			method: http.MethodGet, path: "/sections", query: "limit=10&project_id=6XG",
		},
		{
			action: todoist.ActionGetSection, input: `{"sectionId":"123"}`,
			method: http.MethodGet, path: "/sections/123",
		},
		{
			action: todoist.ActionCreateSection,
			input:  `{"projectId":"6XG","name":"Doing","order":2}`,
			method: http.MethodPost, path: "/sections",
			bodyContains: []string{`"project_id":"6XG"`, `"name":"Doing"`, `"order":2`},
		},
		{
			action: todoist.ActionUpdateSection,
			input:  `{"sectionId":"123","name":null,"isCollapsed":true}`,
			method: http.MethodPost, path: "/sections/123",
			bodyContains: []string{`"name":null`, `"is_collapsed":true`},
		},
		{
			action: todoist.ActionListTasks,
			input:  `{"projectId":"6XG","ids":["7","8"],"label":"focus"}`,
			method: http.MethodGet, path: "/tasks",
			query: "ids=7%2C8&label=focus&project_id=6XG",
		},
		{
			action: todoist.ActionGetTask, input: `{"taskId":"7"}`,
			method: http.MethodGet, path: "/tasks/7",
		},
		{
			action: todoist.ActionCreateTask,
			input:  `{"content":"Review","priority":3,"dueString":"tomorrow"}`,
			method: http.MethodPost, path: "/tasks",
			bodyContains: []string{`"content":"Review"`, `"priority":3`, `"due_string":"tomorrow"`},
			bodyAbsent:   []string{"labels", "assignee_id"},
		},
		{
			action: todoist.ActionUpdateTask,
			input:  `{"taskId":"7","description":null,"assigneeId":null}`,
			method: http.MethodPost, path: "/tasks/7",
			bodyContains: []string{`"description":null`, `"assignee_id":null`},
			bodyAbsent:   []string{"content"},
		},
		{
			action: todoist.ActionCloseTask, input: `{"taskId":"7"}`,
			method: http.MethodPost, path: "/tasks/7/close",
		},
		{
			action: todoist.ActionListComments, input: `{"taskId":"7","limit":5}`,
			method: http.MethodGet, path: "/comments", query: "limit=5&task_id=7",
		},
		{
			action: todoist.ActionGetComment, input: `{"commentId":"45"}`,
			method: http.MethodGet, path: "/comments/45",
		},
		{
			action: todoist.ActionCreateComment,
			input:  `{"content":"Note","taskId":"7","uidsToNotify":[1,2]}`,
			method: http.MethodPost, path: "/comments",
			bodyContains: []string{`"content":"Note"`, `"task_id":"7"`, `"uids_to_notify":[1,2]`},
		},
		{
			action: todoist.ActionUpdateComment, input: `{"commentId":"45","content":"Edited"}`,
			method: http.MethodPost, path: "/comments/45",
			bodyContains: []string{`"content":"Edited"`},
		},
		{
			action: todoist.ActionListLabels, input: `{"limit":50}`,
			method: http.MethodGet, path: "/labels", query: "limit=50",
		},
	}
	definition := todoist.Definition()
	covered := make(map[string]struct{}, len(cases))
	for _, testCase := range cases {
		covered[testCase.action] = struct{}{}
	}
	for _, action := range definition.Actions {
		require.Containsf(t, covered, action.Name,
			"every catalog action needs a wire case: %s", action.Name)
	}

	for _, testCase := range cases {
		t.Run(testCase.action, func(t *testing.T) {
			var capturedMethod, capturedPath, capturedQuery, capturedBody string
			server := newWireServer(t, `{"results":[],"next_cursor":null}`, func(r *http.Request, body []byte) {
				capturedMethod = r.Method
				capturedPath = r.URL.Path
				capturedQuery = r.URL.RawQuery
				capturedBody = string(body)
			})
			executor := runner.NewRESTExecutor(todoist.Domain, server.client(),
				staticCredentialSource{credentials: validCredentials()}, todoist.Handlers())
			result, err := executor.Invoke(context.Background(), invocation(testCase.action, testCase.input))
			require.NoErrorf(t, err, "%s must succeed against the double", testCase.action)
			require.NotEmpty(t, result.Data)

			require.Equal(t, testCase.method, capturedMethod, "method")
			require.Equal(t, testCase.path, capturedPath, "path")
			if testCase.query != "" {
				require.Equal(t, testCase.query, capturedQuery, "query")
			} else {
				require.Empty(t, capturedQuery, "no query expected")
			}
			for _, fragment := range testCase.bodyContains {
				require.Containsf(t, capturedBody, fragment,
					"%s body must contain %s", testCase.action, fragment)
			}
			for _, key := range testCase.bodyAbsent {
				require.NotContainsf(t, capturedBody, `"`+key+`":`,
					"%s body must omit the %q field", testCase.action, key)
			}
		})
	}
}

func TestTodoistCommentNormalizationMergesFileAttachment(t *testing.T) {
	t.Parallel()

	server := newWireServer(t,
		`{"id":"45","content":"n","file_attachment":{"file_name":"a.png"}}`, nil)
	executor := runner.NewRESTExecutor(todoist.Domain, server.client(),
		staticCredentialSource{credentials: validCredentials()}, todoist.Handlers())
	result, err := executor.Invoke(context.Background(), invocation(todoist.ActionGetComment, `{"commentId":"45"}`))
	require.NoError(t, err)
	var payload struct {
		Comment json.RawMessage `json:"comment"`
	}
	require.NoError(t, json.Unmarshal(result.Data, &payload))
	require.Contains(t, string(payload.Comment), `"attachment"`)
	require.Contains(t, string(payload.Comment), `"file_attachment"`)
}

// wireServerDouble is a TLS loopback double that records the latest request.
type wireServerDouble struct {
	server *httptest.Server
}

func newWireServer(t *testing.T, response string, capture func(*http.Request, []byte)) *wireServerDouble {
	t.Helper()
	double := &wireServerDouble{}
	double.server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		if capture != nil {
			capture(r, body)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(response))
	}))
	t.Cleanup(double.server.Close)
	return double
}

func (double *wireServerDouble) client() *runner.RESTClient {
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:   double.server.URL,
		Auth:      todoist.AuthSpec(),
		Transport: insecureTransport(),
	})
	if err != nil {
		panic(err)
	}
	return client
}

// TestIntegerLikeContractMatchesSchema proves the catalog schema and the
// handler agree on integerLike values: every lexical the binding validator
// admits must execute against the provider double, and every rejected one
// must fail validation.
func TestIntegerLikeContractMatchesSchema(t *testing.T) {
	t.Parallel()

	definition := todoist.Definition()
	var inputSchema map[string]any
	for _, action := range definition.Actions {
		if action.Name == todoist.ActionCreateProject {
			inputSchema = action.InputSchema
		}
	}
	require.NotNil(t, inputSchema, "create-project action must exist")

	cases := []struct {
		workspaceID string
		accepted    bool
		wireValue   string
	}{
		{workspaceID: `1`, accepted: true, wireValue: `"workspace_id":1`},
		{workspaceID: `1.0`, accepted: true, wireValue: `"workspace_id":1`},
		{workspaceID: `1e3`, accepted: true, wireValue: `"workspace_id":1000`},
		{workspaceID: `0`, accepted: true, wireValue: `"workspace_id":0`},
		{workspaceID: `9223372036854775807`, accepted: true, wireValue: `"workspace_id":9223372036854775807`},
		{workspaceID: `9223372036854775808`, accepted: false},
		{workspaceID: `-1`, accepted: false},
		{workspaceID: `1.5`, accepted: false},
		{workspaceID: `1e400`, accepted: false},
		{workspaceID: `"12345"`, accepted: true, wireValue: `"workspace_id":12345`},
		{workspaceID: `"1e3"`, accepted: false},
		{workspaceID: `"1234567890123456789"`, accepted: false},
		{workspaceID: `"124"`, accepted: true, wireValue: `"workspace_id":124`},
		{workspaceID: `"abc"`, accepted: false},
		{workspaceID: `null`, accepted: false},
	}
	for _, testCase := range cases {
		input := []byte(`{"name":"AgentTeam","workspaceId":` + testCase.workspaceID + `}`)
		_, schemaErr := binding.ValidateActionInput(inputSchema, input)
		if !testCase.accepted {
			require.Errorf(t, schemaErr, "schema must reject workspaceId=%s", testCase.workspaceID)
			continue
		}
		require.NoErrorf(t, schemaErr, "schema must accept workspaceId=%s", testCase.workspaceID)

		var capturedBody string
		server := newWireServer(t, `{}`, func(_ *http.Request, body []byte) {
			capturedBody = string(body)
		})
		executor := runner.NewRESTExecutor(todoist.Domain, server.client(),
			staticCredentialSource{credentials: validCredentials()}, todoist.Handlers())
		_, err := executor.Invoke(context.Background(), invocation(todoist.ActionCreateProject, string(input)))
		require.NoErrorf(t, err, "schema-accepted workspaceId=%s must execute", testCase.workspaceID)
		require.Containsf(t, capturedBody, testCase.wireValue,
			"workspaceId=%s must normalize on the wire", testCase.workspaceID)
	}
}
