package exec

import (
	"context"
	"encoding/json"
	"errors"
	"reflect"
	"strings"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agentview"
)

type fakeAgentReader struct {
	items []agentview.Snapshot
	err   error
}

type pagedAgentReader struct {
	items []agentview.Snapshot
}

func (source pagedAgentReader) ListAgents(context.Context) ([]agentview.Snapshot, error) {
	panic("paged reader must not materialize the legacy list")
}

func (source pagedAgentReader) ListAgentsPage(_ context.Context, _ string, revision, afterID string, limit int) ([]agentview.Snapshot, bool, string, error) {
	if revision != "" && revision != "test-revision" {
		return nil, false, "", agent.NewPageError(agent.PageErrorStaleRevision, "test revision is stale")
	}
	start := 0
	if afterID != "" {
		for index := range source.items {
			if source.items[index].ID == afterID {
				start = index + 1
			}
		}
	}
	end := start + limit
	hasMore := end < len(source.items)
	if end > len(source.items) {
		end = len(source.items)
	}
	if revision == "" {
		revision = "test-revision"
	}
	return source.items[start:end], hasMore, revision, nil
}

func (source pagedAgentReader) GetAgent(_ context.Context, id string) (agentview.Snapshot, bool, error) {
	for _, item := range source.items {
		if item.ID == id {
			return item, true, nil
		}
	}
	return agentview.Snapshot{}, false, nil
}

func (source fakeAgentReader) ListAgents(context.Context) ([]agentview.Snapshot, error) {
	return append([]agentview.Snapshot(nil), source.items...), source.err
}

func (source fakeAgentReader) GetAgent(_ context.Context, id string) (agentview.Snapshot, bool, error) {
	if source.err != nil {
		return agentview.Snapshot{}, false, source.err
	}
	for _, item := range source.items {
		if item.ID == id {
			return item, true, nil
		}
	}
	return agentview.Snapshot{}, false, nil
}

func TestAgentListCursorQueryMismatchIsTyped(t *testing.T) {
	_, err := decodeAgentListCursor(encodeAgentListCursor("revision", "alpha", "agent-alpha"), "beta")
	var pageErr *agent.PageError
	require.ErrorAs(t, err, &pageErr)
	require.Equal(t, agent.PageErrorQueryMismatch, pageErr.Kind)
	require.True(t, errors.As(err, &pageErr))
}

func TestAgentHandlerUsesPagedReaderWithoutLegacyMaterialization(t *testing.T) {
	handler := agentHandler{source: pagedAgentReader{items: []agentview.Snapshot{
		{ID: "agent-alpha", Name: "Alpha"}, {ID: "agent-beta", Name: "Beta"},
	}}}
	page, err := handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Limit: 1}})
	require.NoError(t, err)
	require.Equal(t, "agent-alpha", page.Data.Agents[0].ID)
	require.True(t, page.Data.HasMore)
	require.Contains(t, page.Data.NextCursor, "a2.")

	second, err := handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Limit: 1, Cursor: page.Data.NextCursor}})
	require.NoError(t, err)
	require.Equal(t, "agent-beta", second.Data.Agents[0].ID)
	require.False(t, second.Data.HasMore)

	_, err = handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Limit: 1, Cursor: "a2.invalid"}})
	require.Equal(t, CodeValidation, classify(err).Code)
	stale := encodeAgentListCursor("old-revision", "", "agent-alpha")
	_, err = handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Limit: 1, Cursor: stale}})
	require.Equal(t, CodeConflict, classify(err).Code)
}

func TestAgentHandlerUsesInjectedSourceForFilterPaginationAndGet(t *testing.T) {
	source := fakeAgentReader{items: []agentview.Snapshot{
		{ID: "agent-beta", Name: "Beta", Runtime: "builtin", Status: "ready"},
		{ID: "agent-alpha", Name: "Alpha", Runtime: "builtin", Status: "ready"},
	}}
	handler := agentHandler{source: source}
	list, err := handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Limit: 1}})
	require.NoError(t, err)
	require.Len(t, list.Data.Agents, 1)
	require.Equal(t, "agent-alpha", list.Data.Agents[0].ID)
	require.True(t, list.Data.HasMore)
	require.NotEmpty(t, list.Data.NextCursor)

	second, err := handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Limit: 1, Cursor: list.Data.NextCursor}})
	require.NoError(t, err)
	require.Equal(t, []string{"agent-beta"}, []string{second.Data.Agents[0].ID})
	require.False(t, second.Data.HasMore)

	_, err = handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Cursor: "99"}})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)
	_, err = handler.list(context.Background(), actionScopedJSON[agentListRequest]{Body: agentListRequest{Query: "other", Cursor: list.Data.NextCursor}})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)

	got, err := handler.get(context.Background(), actionScopedJSON[agentGetRequest]{Body: agentGetRequest{AgentID: "agent-alpha"}})
	require.NoError(t, err)
	require.Equal(t, "agent-alpha", got.Data.Agent.ID)
}

func TestAgentHandlerGetMissingAgentReturnsNotFound(t *testing.T) {
	_, err := (agentHandler{source: fakeAgentReader{}}).get(context.Background(), actionScopedJSON[agentGetRequest]{Body: agentGetRequest{AgentID: "missing"}})
	require.Error(t, err)
	require.Equal(t, CodeNotFound, classify(err).Code)
}

func TestAgentResponseUsesCanonicalSnapshotJSON(t *testing.T) {
	snapshot := agentExecSnapshot{
		ID: "agent-1", Name: "Agent", Runtime: "codex", ConfiguredHeaderNames: []string{"x-region"},
		Status: "ready", BackendRegistered: true,
		Policy: &agentExecPolicy{MaxConcurrentTasks: 2, AllowedPathCount: 1},
	}
	got, err := json.Marshal(agentGetResult{Agent: snapshot})
	require.NoError(t, err)
	require.JSONEq(t, `{"agent":{
		"id":"agent-1","name":"Agent","runtime":"codex","configuredHeaderNames":["x-region"],
		"policy":{"maxConcurrentTasks":2,"allowedPathCount":1},
		"status":"ready","backendRegistered":true
	}}`, string(got))
}

func TestAgentHeadersRequireExactResultPaths(t *testing.T) {
	require.NoError(t, validateConcreteContractType(reflect.TypeOf(agentListResult{}), "output data"))
	require.NoError(t, validateConcreteContractType(reflect.TypeOf(agentGetResult{}), "output data"))

	for _, value := range []any{
		struct {
			ConfiguredHeaderNames map[string]string `json:"configuredHeaderNames"`
		}{},
		struct {
			ConfiguredHeaderNames map[string]any `json:"configuredHeaderNames"`
		}{},
	} {
		err := validateConcreteContractType(reflect.TypeOf(value), "output data")
		require.Error(t, err)
		require.ErrorContains(t, err, "output data")
	}
}

func TestAgentExecProjectionSchemaOmitsSecretsAndFilesystemDetails(t *testing.T) {
	for _, describe := range []func() (actionDescriptor, error){
		func() (actionDescriptor, error) { return describe(agentListSpec) },
		func() (actionDescriptor, error) { return describe(agentGetSpec) },
	} {
		descriptor, err := describe()
		require.NoError(t, err)
		raw := string(descriptor.OutputSchema)
		require.Contains(t, raw, `"configuredHeaderNames"`)
		require.NotContains(t, raw, `"headers"`)
		require.NotContains(t, raw, `"baseUrl"`)
		require.NotContains(t, raw, `"endpoint"`)
		require.NotContains(t, raw, `"allowedPaths"`)
	}
}

func TestProjectAgentSnapshotRedactsCredentialAndLocalPathValues(t *testing.T) {
	snapshot := projectAgentSnapshot(agentview.Snapshot{
		ID: "agent-1", Runtime: "codex", BaseURL: "https://user:pass@example.test/api?token=secret#fragment",
		Endpoint: "http://127.0.0.1:7777/internal?access_token=secret", APIKeyConfigured: true,
		Headers: agentview.Headers{"Authorization": "Bearer secret", "X-API-Key": "key-secret", "X-Custom-Provider": "value-secret"},
		Policy:  &agentview.Policy{AllowedPaths: []string{"/home/private/repository"}},
	})
	raw, err := json.Marshal(snapshot)
	require.NoError(t, err)
	text := string(raw)
	require.NotContains(t, text, "secret")
	require.NotContains(t, text, "private/repository")
	require.NotContains(t, text, "baseUrl")
	require.NotContains(t, text, "endpoint")
	require.Contains(t, text, `"apiKeyConfigured":true`)
	require.Contains(t, text, `"configuredHeaderNames":["authorization","x-api-key","x-custom-provider"]`)
	require.Contains(t, text, `"allowedPathCount":1`)
}

func TestAgentSchemasUseCanonicalFieldsAndConstraints(t *testing.T) {
	get, err := describe(agentGetSpec)
	require.NoError(t, err)
	require.Contains(t, string(get.InputSchema), `"agentId"`)
	require.NotContains(t, string(get.InputSchema), `"agent_id"`)
	getSchema := mustCompile(t, get.InputSchema)
	require.NoError(t, validateInstance(getSchema, json.RawMessage(`{"agentId":"agt_123"}`)))
	require.Error(t, validateInstance(getSchema, json.RawMessage(`{"agentId":"   "}`)))

	list, err := describe(agentListSpec)
	require.NoError(t, err)
	require.Contains(t, string(list.OutputSchema), `"nextCursor"`)
	require.Contains(t, string(list.OutputSchema), `"hasMore"`)
	require.NotContains(t, string(list.OutputSchema), `"next_cursor"`)
	require.NotContains(t, string(list.OutputSchema), `"has_more"`)
	listSchema := mustCompile(t, list.InputSchema)
	require.NoError(t, validateInstance(listSchema, json.RawMessage(`{"limit":100}`)))
	require.Error(t, validateInstance(listSchema, json.RawMessage(`{"limit":0}`)))
	require.Error(t, validateInstance(listSchema, json.RawMessage(`{"limit":101}`)))
}

type fakeAgentRuntime struct {
	lifecycleRequest   *agentLifecycleRequest
	createRequest      *agentCreateRequest
	updateRequest      *agentUpdateRequest
	deleteRequest      *agentDeleteRequest
	duplicateRequest   *agentDuplicateRequest
	actions            []string
	lifecycleResponse  agentLifecycleResponse
	structuralResponse agentStructuralResponse
	err                error
}

func (runtime *fakeAgentRuntime) lifecycle(_ context.Context, action string, in agentLifecycleRequest) (agentLifecycleResponse, error) {
	runtime.lifecycleRequest = &in
	runtime.actions = append(runtime.actions, action)
	return runtime.lifecycleResponse, runtime.err
}
func (runtime *fakeAgentRuntime) create(_ context.Context, in agentCreateRequest) (agentStructuralResponse, error) {
	runtime.createRequest = &in
	runtime.actions = append(runtime.actions, "create")
	return runtime.structuralResponse, runtime.err
}
func (runtime *fakeAgentRuntime) update(_ context.Context, in agentUpdateRequest) (agentStructuralResponse, error) {
	runtime.updateRequest = &in
	runtime.actions = append(runtime.actions, "update")
	return runtime.structuralResponse, runtime.err
}
func (runtime *fakeAgentRuntime) delete(_ context.Context, in agentDeleteRequest) (agentStructuralResponse, error) {
	runtime.deleteRequest = &in
	runtime.actions = append(runtime.actions, "delete")
	return runtime.structuralResponse, runtime.err
}
func (runtime *fakeAgentRuntime) duplicate(_ context.Context, in agentDuplicateRequest) (agentStructuralResponse, error) {
	runtime.duplicateRequest = &in
	runtime.actions = append(runtime.actions, "duplicate")
	return runtime.structuralResponse, runtime.err
}

func TestAgentLifecycleSchemasUseCanonicalFieldsAndConstraints(t *testing.T) {
	for _, spec := range []struct {
		name string
		sp   func() (actionDescriptor, error)
	}{
		{"start", func() (actionDescriptor, error) { return describe(agentStartSpec) }},
		{"stop", func() (actionDescriptor, error) { return describe(agentStopSpec) }},
		{"restart", func() (actionDescriptor, error) { return describe(agentRestartSpec) }},
	} {
		t.Run(spec.name, func(t *testing.T) {
			t.Parallel()

			descriptor, err := spec.sp()
			require.NoError(t, err)
			require.Contains(t, string(descriptor.InputSchema), `"requestId"`)
			require.Contains(t, string(descriptor.InputSchema), `"agentId"`)
			require.NotContains(t, string(descriptor.InputSchema), `"request_id"`)
			require.Contains(t, string(descriptor.OutputSchema), `"status"`)

			schema := mustCompile(t, descriptor.InputSchema)
			require.NoError(t, validateInstance(schema, json.RawMessage(`{"requestId":"req-1","agentId":"agt_123"}`)))
			require.Error(t, validateInstance(schema, json.RawMessage(`{"requestId":"","agentId":"agt_123"}`)))
			require.Error(t, validateInstance(schema, json.RawMessage(`{"requestId":"`+strings.Repeat("a", 129)+`","agentId":"agt_123"}`)))
		})
	}
}

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

	body := agentLifecycleRequest{RequestID: "req-1", AgentID: "agt_123"}

	startRemote := &fakeAgentRuntime{lifecycleResponse: agentLifecycleResponse{Status: "started"}}
	startResult, err := agentHandler{remote: startRemote}.start(context.Background(), actionScopedJSON[agentLifecycleRequest]{Body: body})
	require.NoError(t, err)
	require.Equal(t, "started", startResult.Data.Status)
	require.Equal(t, &body, startRemote.lifecycleRequest, "the request body is forwarded unchanged")

	stopRemote := &fakeAgentRuntime{lifecycleResponse: agentLifecycleResponse{Status: "stopping"}}
	stopResult, err := agentHandler{remote: stopRemote}.stop(context.Background(), actionScopedJSON[agentLifecycleRequest]{Body: body})
	require.NoError(t, err)
	require.Equal(t, "stopping", stopResult.Data.Status)

	restartRemote := &fakeAgentRuntime{lifecycleResponse: agentLifecycleResponse{Status: "restarted"}}
	restartResult, err := agentHandler{remote: restartRemote}.restart(context.Background(), actionScopedJSON[agentLifecycleRequest]{Body: body})
	require.NoError(t, err)
	require.Equal(t, "restarted", restartResult.Data.Status)
}

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

	create := mustActionInputSchema(t, agentCreateSpec)
	require.NoError(t, validateInstance(create, json.RawMessage(`{
		"requestId":"req-1","name":"researcher","machineId":"mac-1","runtime":"codex",
		"description":"research","model":"gpt-5","reasoningEffort":"high",
		"modelProfileId":"profile-1","channelIds":["chn-1"]
	}`)))
	require.Error(t, validateInstance(create, json.RawMessage(`{"requestId":"req-1","name":"researcher","machineId":"mac-1"}`)))

	update := mustActionInputSchema(t, agentUpdateSpec)
	for _, raw := range []string{
		`{"requestId":"req-1","agentId":"agt-1","description":null}`,
		`{"requestId":"req-1","agentId":"agt-1","model":"gpt-5"}`,
		`{"requestId":"req-1","agentId":"agt-1","reasoningEffort":"","modelProfileId":null}`,
	} {
		require.NoError(t, validateInstance(update, json.RawMessage(raw)), raw)
	}
	for _, raw := range []string{
		`{"requestId":"req-1","agentId":"agt-1"}`,
		`{"requestId":"req-1","agentId":"agt-1","description":42}`,
		`{"requestId":"req-1","agentId":"agt-1","model":{"value":"gpt-5"}}`,
		`{"requestId":"req-1","agentId":"agt-1","channelIds":["chn-1"]}`,
	} {
		require.Error(t, validateInstance(update, json.RawMessage(raw)), raw)
	}

	deleteSchema := mustActionInputSchema(t, agentDeleteSpec)
	require.NoError(t, validateInstance(deleteSchema, json.RawMessage(`{"requestId":"req-1","agentIds":["agt-2","agt-1"]}`)))
	require.Error(t, validateInstance(deleteSchema, json.RawMessage(`{"requestId":"req-1","agentIds":[]}`)))
	require.Error(t, validateInstance(deleteSchema, json.RawMessage(`{"requestId":"req-1","agentIds":[""]}`)))
	require.Error(t, validateInstance(deleteSchema, json.RawMessage(`{"requestId":"req-1","agentIds":["1","2","3","4","5","6","7","8","9","10","11"]}`)))

	duplicate := mustActionInputSchema(t, agentDuplicateSpec)
	require.NoError(t, validateInstance(duplicate, json.RawMessage(`{"requestId":"req-1","sourceAgentId":"agt-1"}`)))
	require.NoError(t, validateInstance(duplicate, json.RawMessage(`{
		"requestId":"req-1","sourceAgentId":"agt-1","count":10,"mode":"with_resources",
		"machineId":"mac-1","runtime":"codex","channelIds":["chn-1"]
	}`)))
	require.Error(t, validateInstance(duplicate, json.RawMessage(`{"requestId":"req-1","sourceAgentId":"agt-1","count":0}`)))
	require.Error(t, validateInstance(duplicate, json.RawMessage(`{"requestId":"req-1","sourceAgentId":"agt-1","count":11}`)))
	require.Error(t, validateInstance(duplicate, json.RawMessage(`{"requestId":"req-1","sourceAgentId":"agt-1","mode":"full"}`)))

	for _, forbidden := range []string{"apiKey", "baseURL", "headers", "session", "role", "createFromRole", "applyRole", "reset"} {
		raw := json.RawMessage(`{"requestId":"req-1","name":"researcher","machineId":"mac-1","runtime":"codex","` + forbidden + `":"secret"}`)
		require.Error(t, validateInstance(create, raw), forbidden)
	}
}

func mustActionInputSchema[I, O any](t *testing.T, spec actionSpec[I, O]) *jsonschema.Schema {
	t.Helper()
	descriptor, err := describe(spec)
	require.NoError(t, err)
	return mustCompile(t, descriptor.InputSchema)
}

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

	var request agentUpdateRequest
	require.NoError(t, json.Unmarshal([]byte(`{"requestId":"req-1","agentId":"agt-1"}`), &request))
	require.False(t, request.Description.Set)

	require.NoError(t, json.Unmarshal([]byte(`{"requestId":"req-1","agentId":"agt-1","description":null}`), &request))
	require.True(t, request.Description.Set)
	require.Nil(t, request.Description.Value)
	raw, err := json.Marshal(request)
	require.NoError(t, err)
	require.JSONEq(t, `{"requestId":"req-1","agentId":"agt-1","description":null}`, string(raw))

	require.NoError(t, json.Unmarshal([]byte(`{"requestId":"req-1","agentId":"agt-1","description":"research"}`), &request))
	require.True(t, request.Description.Set)
	require.Equal(t, "research", *request.Description.Value)
	raw, err = json.Marshal(request)
	require.NoError(t, err)
	require.JSONEq(t, `{"requestId":"req-1","agentId":"agt-1","description":"research"}`, string(raw))

	for _, value := range []string{`42`, `true`, `{}`, `[]`} {
		var invalid agentUpdateRequest
		require.Error(t, json.Unmarshal([]byte(`{"requestId":"req-1","agentId":"agt-1","description":`+value+`}`), &invalid), value)
	}
}

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

	response := agentStructuralResponse{Status: "pending", ApprovalID: "appr-1"}
	runtime := &fakeAgentRuntime{structuralResponse: response}
	handler := agentHandler{remote: runtime}

	create := agentCreateRequest{RequestID: "req-create", Name: "researcher", MachineID: "mac-1", Runtime: "codex"}
	result, err := handler.create(context.Background(), actionScopedJSON[agentCreateRequest]{Body: create})
	require.NoError(t, err)
	require.Equal(t, response, result.Data)
	require.Equal(t, &create, runtime.createRequest)

	update := agentUpdateRequest{
		RequestID: "req-update", AgentID: "agt-1",
		Description: agentOptionalString{Set: true, Value: nil},
	}
	result, err = handler.update(context.Background(), actionScopedJSON[agentUpdateRequest]{Body: update})
	require.NoError(t, err)
	require.Equal(t, response, result.Data)
	require.Equal(t, &update, runtime.updateRequest)

	deleteRequest := agentDeleteRequest{RequestID: "req-delete", AgentIDs: []string{"agt-2", "agt-1"}}
	result, err = handler.delete(context.Background(), actionScopedJSON[agentDeleteRequest]{Body: deleteRequest})
	require.NoError(t, err)
	require.Equal(t, response, result.Data)
	require.Equal(t, &deleteRequest, runtime.deleteRequest)

	duplicate := agentDuplicateRequest{RequestID: "req-duplicate", SourceAgentID: "agt-1"}
	result, err = handler.duplicate(context.Background(), actionScopedJSON[agentDuplicateRequest]{Body: duplicate})
	require.NoError(t, err)
	require.Equal(t, response, result.Data)
	require.Equal(t, &duplicate, runtime.duplicateRequest)
}

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

	for _, item := range []struct {
		spec any
		want string
	}{
		{agentCreateSpec.Examples[0].Input, `{"requestId":"req-create-01","name":"researcher","machineId":"mac_123","runtime":"codex"}`},
		{agentDeleteSpec.Examples[0].Input, `{"requestId":"req-delete-01","agentIds":["agt_123"]}`},
		{agentDuplicateSpec.Examples[0].Input, `{"requestId":"req-duplicate-01","sourceAgentId":"agt_123"}`},
	} {
		raw, err := json.Marshal(item.spec)
		require.NoError(t, err)
		require.JSONEq(t, item.want, string(raw))
	}

	raw, err := json.Marshal(agentUpdateSpec.Examples[0].Input)
	require.NoError(t, err)
	require.JSONEq(t, `{
		"requestId":"req-update-01","agentId":"agt_123","description":"research tasks"
	}`, string(raw))
}

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

	descriptor, err := describe(agentCreateSpec)
	require.NoError(t, err)
	output := mustCompile(t, descriptor.OutputSchema)
	require.NoError(t, validateInstance(output, json.RawMessage(`{
		"data":{"status":"pending","approvalId":"appr-1"}
	}`)))
	require.Error(t, validateInstance(output, json.RawMessage(`{
		"data":{"status":"applied","approvalId":"appr-1"}
	}`)))
	require.Error(t, validateInstance(output, json.RawMessage(`{
		"data":{"status":"pending"}
	}`)))
}
