package ones_test

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/santhosh-tekuri/jsonschema/v6"
	"github.com/stretchr/testify/require"

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

func findOutputSchema(t *testing.T, actionName string) map[string]any {
	t.Helper()
	for _, action := range ones.Definition().Actions {
		if action.Name == actionName {
			require.NotNil(t, action.OutputSchema, "%s must declare an output schema", actionName)
			return action.OutputSchema
		}
	}
	t.Fatalf("action %s not found", actionName)
	return nil
}

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

	wire := newWireServer(t, 200, `{"user":{"uuid":"DU6krHBN","token":"SECRET_TOKEN_VALUE"},`+
		`"teams":[{"uuid":"3pDzCwAe"}],"org":{"uuid":"369VHsHp"}}`)
	result, err := invoke(t, wire, "get-auth-info", `{}`)
	require.NoError(t, err)
	require.NotContains(t, string(result), "SECRET_TOKEN_VALUE")
	require.NotContains(t, string(result), "token")
	require.Contains(t, string(result), `"uuid":"DU6krHBN"`)
	require.Contains(t, string(result), "3pDzCwAe")
}

func findInputSchema(t *testing.T, actionName string) map[string]any {
	t.Helper()
	for _, action := range ones.Definition().Actions {
		if action.Name == actionName {
			require.NotNil(t, action.InputSchema, "%s must declare an input schema", actionName)
			return action.InputSchema
		}
	}
	t.Fatalf("action %s not found", actionName)
	return nil
}

// TestSchemaAndHandlerAcceptTheSameIdentifiers runs the binding validator
// and the executor against the same lexical inputs: whatever the schema
// admits must execute, whatever it rejects must never reach the wire.
func TestSchemaAndHandlerAcceptTheSameIdentifiers(t *testing.T) {
	t.Parallel()

	spaceCases := []struct {
		value   string
		accept  bool
		routeTo string
	}{
		{value: `"Gj7aPmA1"`, accept: true, routeTo: "/space/Gj7aPmA1"},
		{value: `"DU6krHBNXuPAbpv8"`, accept: false},
		{value: `"short"`, accept: false},
		{value: `"DU6krHBNXuPAbpv89"`, accept: false},
		{value: `"../evil"`, accept: false},
		{value: `""`, accept: false},
	}
	spaceSchema := findInputSchema(t, ones.ActionGetSpace)
	for _, testCase := range spaceCases {
		input := []byte(`{"team_uuid":"3pDzCwAe","space_uuid":` + testCase.value + `}`)
		_, schemaErr := binding.ValidateActionInput(spaceSchema, input)
		if !testCase.accept {
			require.Errorf(t, schemaErr, "schema must reject space_uuid=%s", testCase.value)
			continue
		}
		require.NoErrorf(t, schemaErr, "schema must accept space_uuid=%s", testCase.value)

		wire := newWireServer(t, 200, `{}`)
		_, err := invoke(t, wire, "get-space", string(input))
		require.NoErrorf(t, err, "schema-accepted space_uuid=%s must execute", testCase.value)
		require.True(t, strings.HasSuffix(wire.last().path, testCase.routeTo),
			"space_uuid=%s must route to %s", testCase.value, testCase.routeTo)
	}

	taskCases := []struct {
		value   string
		accept  bool
		routeTo string
	}{
		{value: `"46"`, accept: true, routeTo: "/task/46/info"},
		{value: `46`, accept: true, routeTo: "/task/46/info"},
		{value: `"DU6krHBNJPoQgfJv"`, accept: true, routeTo: "/task/DU6krHBNJPoQgfJv/info"},
		{value: `"Gj7aPmA1"`, accept: false},
		{value: `"../evil"`, accept: false},
		{value: `-1`, accept: false},
		{value: `"toolongidentifier1234567890"`, accept: false},
	}
	taskSchema := findInputSchema(t, ones.ActionGetTask)
	for _, testCase := range taskCases {
		input := []byte(`{"team_uuid":"3pDzCwAe","task":` + testCase.value + `}`)
		_, schemaErr := binding.ValidateActionInput(taskSchema, input)
		if !testCase.accept {
			require.Errorf(t, schemaErr, "schema must reject task=%s", testCase.value)
			continue
		}
		require.NoErrorf(t, schemaErr, "schema must accept task=%s", testCase.value)

		wire := newWireServer(t, 200, `{}`)
		_, err := invoke(t, wire, "get-task", string(input))
		require.NoErrorf(t, err, "schema-accepted task=%s must execute", testCase.value)
		require.True(t, strings.HasSuffix(wire.last().path, testCase.routeTo),
			"task=%s must route to %s", testCase.value, testCase.routeTo)
	}
}

// TestOfficialFieldBoundariesMatchTheSchema locks the documented caps: the
// project name fits 128, the work item summary 1024, the sprint title 32 —
// and one past each cap is rejected.
func TestOfficialFieldBoundariesMatchTheSchema(t *testing.T) {
	t.Parallel()

	longSummary := strings.Repeat("s", 1024)
	longName := strings.Repeat("n", 128)
	longSprintTitle := strings.Repeat("t", 32)
	boundaries := []struct {
		name   string
		action string
		input  string
	}{
		{
			name: "summary-at-1024", action: ones.ActionCreateTask,
			input: `{"team_uuid":"3pDzCwAe","tasks":[{"uuid":"DU6krHBNNKSnnHNj","assign":"DU6krHBN","project_uuid":"DU6krHBNXuPAbpv8","issue_type_uuid":"GLLfcQxq","summary":"` + longSummary + `","priority":"7tKAV46c"}]}`,
		},
		{
			name: "project-name-at-128", action: ones.ActionCreateProject,
			input: `{"team_uuid":"3pDzCwAe","project":{"uuid":"DU6krHBN12345678","assign":"DU6krHBN","name":"` + longName + `"}}`,
		},
		{
			name: "sprint-title-at-32", action: ones.ActionCreateSprint,
			input: `{"team_uuid":"3pDzCwAe","project_uuid":"DU6krHBNXuPAbpv8","sprints":[{"title":"` + longSprintTitle + `"}]}`,
		},
	}
	for _, boundary := range boundaries {
		_, err := binding.ValidateActionInput(findInputSchema(t, boundary.action), []byte(boundary.input))
		require.NoErrorf(t, err, "%s must be accepted", boundary.name)
	}
	rejections := []struct {
		name   string
		action string
		input  string
	}{
		{
			name: "summary-past-1024", action: ones.ActionCreateTask,
			input: `{"team_uuid":"3pDzCwAe","tasks":[{"uuid":"DU6krHBNNKSnnHNj","assign":"DU6krHBN","project_uuid":"DU6krHBNXuPAbpv8","issue_type_uuid":"GLLfcQxq","summary":"` + longSummary + `x","priority":"7tKAV46c"}]}`,
		},
		{
			name: "project-name-past-128", action: ones.ActionCreateProject,
			input: `{"team_uuid":"3pDzCwAe","project":{"uuid":"DU6krHBN12345678","assign":"DU6krHBN","name":"` + longName + `x"}}`,
		},
		{
			name: "sprint-title-past-32", action: ones.ActionCreateSprint,
			input: `{"team_uuid":"3pDzCwAe","project_uuid":"DU6krHBNXuPAbpv8","sprints":[{"title":"` + longSprintTitle + `x"}]}`,
		},
		{
			name: "sprint-unknown-writable-field", action: ones.ActionCreateSprint,
			input: `{"team_uuid":"3pDzCwAe","project_uuid":"DU6krHBNXuPAbpv8","sprints":[{"title":"s","mystery":1}]}`,
		},
	}
	for _, rejection := range rejections {
		_, err := binding.ValidateActionInput(findInputSchema(t, rejection.action), []byte(rejection.input))
		require.Errorf(t, err, "%s must be rejected", rejection.name)
	}
}

// TestGetAuthInfoFailsClosedOnMalformedEnvelopes locks the strict rebuild:
// a missing, non-object, or identity-less user is an upstream failure, never
// a partial passthrough.
func TestGetAuthInfoFailsClosedOnMalformedEnvelopes(t *testing.T) {
	t.Parallel()

	for name, body := range map[string]string{
		"no-user":         `{"teams":[]}`,
		"user-not-object": `{"user":"nope"}`,
		"user-no-uuid":    `{"user":{"name":"u"}}`,
	} {
		wire := newWireServer(t, 200, body)
		_, err := invoke(t, wire, "get-auth-info", `{}`)
		require.Errorf(t, err, "envelope %s must fail closed", name)
	}
}

// TestGetAuthInfoOfficialSampleConformsToOutputSchema runs the documented
// token_info sample through the handler and validates the rebuilt envelope
// against the declared output schema, so the projection and the contract
// cannot drift apart.
func TestGetAuthInfoOfficialSampleConformsToOutputSchema(t *testing.T) {
	t.Parallel()

	compiler := jsonschema.NewCompiler()
	compiler.DefaultDraft(jsonschema.Draft2020)
	require.NoError(t, compiler.AddResource("ones-auth-output.json",
		findOutputSchema(t, "get-auth-info")))
	compiled, err := compiler.Compile("ones-auth-output.json")
	require.NoError(t, err)

	wire := newWireServer(t, 200, `{"user":{"uuid":"Gq8ZZZ7F","email":"u@example.com",`+
		`"name":"u","name_pinyin":"u","title":"","avatar":"","phone":"",`+
		`"create_time":1547538969719424,"status":1,"channel":"ch",`+
		`"license_types":[1,2],"token":"SECRET"},"teams":[{"uuid":"U66S45tG"}],`+
		`"org":{"uuid":"369VHsHp","name":"tim"}}`)
	result, err := invoke(t, wire, "get-auth-info", `{}`)
	require.NoError(t, err)
	require.NotContains(t, string(result), "SECRET")
	var decoded any
	require.NoError(t, json.Unmarshal(result, &decoded))
	require.NoError(t, compiled.Validate(decoded))
}

// TestExactIdentifierShapesLockResidualContracts pins the round-3 shapes:
// member/product uuids are 8 characters, task ids are 16, the project ids
// filter is a comma-separated uuid16 set, and descriptions cap at 65536.
func TestExactIdentifierShapesLockResidualContracts(t *testing.T) {
	t.Parallel()

	accepts := []struct {
		name   string
		action string
		input  string
	}{
		{
			name: "members-uuid8", action: ones.ActionCreateProject,
			input: `{"team_uuid":"3pDzCwAe","project":{"uuid":"DU6krHBN12345678","assign":"DU6krHBN","name":"p"},"members":["DU6krHBN"]}`,
		},
		{
			name: "task-ids-uuid16", action: ones.ActionGetTasks,
			input: `{"team_uuid":"3pDzCwAe","ids":["DU6krHBNJPoQgfJv","DU6krHBNNKSnnHNI"]}`,
		},
		{
			name: "product-uuids-8", action: ones.ActionCreateTask,
			input: `{"team_uuid":"3pDzCwAe","tasks":[{"uuid":"DU6krHBNNKSnnHNj","assign":"DU6krHBN","project_uuid":"DU6krHBNXuPAbpv8","issue_type_uuid":"GLLfcQxq","summary":"s","priority":"7tKAV46c","product_uuids":["7tKAV46c"]}]}`,
		},
		{
			name: "project-ids-csv16", action: ones.ActionListProjects,
			input: `{"team_uuid":"3pDzCwAe","ids":"RGzJnspW7PFk7mZk,DU6krHBNJEEeoG8G"}`,
		},
		{
			name: "desc-at-65536", action: ones.ActionUpdateTask,
			input: `{"team_uuid":"3pDzCwAe","tasks":[{"uuid":"DU6krHBNNKSnnHNj","desc":"` +
				strings.Repeat("d", 65536) + `"}]}`,
		},
		{
			name: "parent-page-uuid8", action: ones.ActionCreatePage,
			input: `{"team_uuid":"3pDzCwAe","space_uuid":"Gj7aPmA1","title":"t","content":"c","parent_page_uuid":"4VSnJJmk"}`,
		},
	}
	for _, acceptance := range accepts {
		_, err := binding.ValidateActionInput(
			findInputSchema(t, acceptance.action), []byte(acceptance.input))
		require.NoErrorf(t, err, "%s must be accepted", acceptance.name)
	}
	rejects := []struct {
		name   string
		action string
		input  string
	}{
		{
			name: "members-not-uuid8", action: ones.ActionCreateProject,
			input: `{"team_uuid":"3pDzCwAe","project":{"uuid":"DU6krHBN12345678","assign":"DU6krHBN","name":"p"},"members":["DU6krHBNXuPAbpv8"]}`,
		},
		{
			name: "task-ids-8char", action: ones.ActionGetTasks,
			input: `{"team_uuid":"3pDzCwAe","ids":["Gj7aPmA1"]}`,
		},
		{
			name: "product-uuids-16char", action: ones.ActionCreateTask,
			input: `{"team_uuid":"3pDzCwAe","tasks":[{"uuid":"DU6krHBNNKSnnHNj","assign":"DU6krHBN","project_uuid":"DU6krHBNXuPAbpv8","issue_type_uuid":"GLLfcQxq","summary":"s","priority":"7tKAV46c","product_uuids":["DU6krHBNXuPAbpv8"]}]}`,
		},
		{
			name: "project-ids-mixed-widths", action: ones.ActionListProjects,
			input: `{"team_uuid":"3pDzCwAe","ids":"RGzJnspW7PFk7mZk,Gj7aPmA1"}`,
		},
		{
			name: "desc-past-65536", action: ones.ActionUpdateTask,
			input: `{"team_uuid":"3pDzCwAe","tasks":[{"uuid":"DU6krHBNNKSnnHNj","desc":"` +
				strings.Repeat("d", 65537) + `"}]}`,
		},
		{
			name: "parent-page-16char", action: ones.ActionCreatePage,
			input: `{"team_uuid":"3pDzCwAe","space_uuid":"Gj7aPmA1","title":"t","content":"c","parent_page_uuid":"DU6krHBNJPoQgfJv"}`,
		},
	}
	for _, rejection := range rejects {
		_, err := binding.ValidateActionInput(
			findInputSchema(t, rejection.action), []byte(rejection.input))
		require.Errorf(t, err, "%s must be rejected", rejection.name)
	}
}
