package webagent

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"
	"unicode/utf8"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/service/agentapproval"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
)

type operationDuplicateCall struct {
	UserID        string
	WorkspaceID   string
	SourceAgentID string
	Input         DuplicateAgentInput
}

type operationDuplicateServiceFake struct {
	operationAgentServiceFake
	calls  []operationDuplicateCall
	result agent.AppAgent
	err    error
}

func (fake *operationDuplicateServiceFake) DuplicateAgent(_ context.Context, userID, workspaceID, sourceAgentID string, input DuplicateAgentInput) (agent.AppAgent, *dto.WebAgentResourceCopyResponse, error) {
	fake.calls = append(fake.calls, operationDuplicateCall{
		UserID: userID, WorkspaceID: workspaceID, SourceAgentID: sourceAgentID, Input: input,
	})
	return fake.result, nil, fake.err
}

type operationDuplicateDependenciesFake struct {
	agents         []agent.AppAgent
	owner          identity.User
	inventoryCalls int
	identityCalls  int
	inventoryErr   error
	identityErr    error
}

func (fake *operationDuplicateDependenciesFake) ListOwnedAgentsForWorkspace(context.Context, string, string) ([]agent.AppAgent, error) {
	fake.inventoryCalls++
	return fake.agents, fake.inventoryErr
}

func (fake *operationDuplicateDependenciesFake) FindUserByID(context.Context, string) (identity.User, error) {
	fake.identityCalls++
	return fake.owner, fake.identityErr
}

func TestOperationExecutorDuplicateRejectsInvalidItemBeforeAnyCalls(t *testing.T) {
	proposal := agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 2, Mode: "config_only",
	}
	for _, test := range []struct {
		name   string
		mutate func(*agentops.Operation, *agentops.OperationItem)
	}{
		{name: "negative index", mutate: func(_ *agentops.Operation, item *agentops.OperationItem) {
			item.ItemIndex = -1
		}},
		{name: "index equal to count", mutate: func(_ *agentops.Operation, item *agentops.OperationItem) {
			item.ItemIndex = proposal.Count
		}},
		{name: "item mismatch", mutate: func(_ *agentops.Operation, item *agentops.OperationItem) {
			item.Input = json.RawMessage(`{"sourceAgentId":"agt_other","count":2,"mode":"config_only"}`)
		}},
		{name: "raw credential in item", mutate: func(_ *agentops.Operation, item *agentops.OperationItem) {
			item.Input = json.RawMessage(`{"sourceAgentId":"agt_source","count":2,"mode":"config_only","session":"secret"}`)
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			service := &operationDuplicateServiceFake{}
			dependencies := &operationDuplicateDependenciesFake{}
			executor := NewOperationExecutor(service,
				WithOperationAgentInventory(dependencies),
				WithOperationOwnerIdentityFinder(dependencies),
			)
			op, item := duplicateOperationFixture(t, proposal, 0)
			test.mutate(op, &item)

			result, err := executor.ExecuteItem(context.Background(), op, item)

			require.Error(t, err)
			require.Nil(t, result)
			require.Zero(t, dependencies.inventoryCalls)
			require.Zero(t, dependencies.identityCalls)
			require.Empty(t, service.calls)
			require.Empty(t, service.createCalls)
			require.Empty(t, service.updateCalls)
			require.Empty(t, service.deleteCalls)
		})
	}
}

func TestOperationExecutorDuplicateRejectsFrozenIdentityMismatchBeforeAnyCalls(t *testing.T) {
	proposal := agentapproval.DuplicateProposal{SourceAgentID: "agt_source", Count: 1}
	for _, test := range []struct {
		name   string
		mutate func(*agentops.Operation)
	}{
		{name: "action", mutate: func(op *agentops.Operation) {
			op.Action = agentapproval.ActionDelete
		}},
		{name: "owner", mutate: func(op *agentops.Operation) {
			op.OwnerUserID = "usr_other"
		}},
	} {
		t.Run(test.name, func(t *testing.T) {
			service := &operationDuplicateServiceFake{}
			dependencies := &operationDuplicateDependenciesFake{}
			executor := NewOperationExecutor(service,
				WithOperationAgentInventory(dependencies),
				WithOperationOwnerIdentityFinder(dependencies),
			)
			op, item := duplicateOperationFixture(t, proposal, 0)
			test.mutate(op)

			result, err := executor.ExecuteItem(context.Background(), op, item)

			require.Error(t, err)
			require.Nil(t, result)
			require.Zero(t, dependencies.inventoryCalls)
			require.Zero(t, dependencies.identityCalls)
			require.Empty(t, service.calls)
		})
	}
}

func TestOperationExecutorDuplicateConfigOnlyUsesFrozenTargetWithoutIdentityLookup(t *testing.T) {
	service := &operationDuplicateServiceFake{result: agent.AppAgent{ID: "agt_copy"}}
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{{ID: "agt_source", Name: "source", DisplayName: "Source"}},
	}
	executor := NewOperationExecutor(service,
		WithOperationAgentInventory(dependencies),
		WithOperationOwnerIdentityFinder(dependencies),
	)
	proposal := agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 1, Mode: "config_only",
		MachineID: "mach_frozen", Runtime: "codex", ChannelIDs: []string{"chn_1", "chn_2"},
	}
	op, item := duplicateOperationFixture(t, proposal, 0)

	result, err := executor.ExecuteItem(context.Background(), op, item)

	require.NoError(t, err)
	require.JSONEq(t, `{"agentId":"agt_copy"}`, string(result))
	require.Equal(t, 1, dependencies.inventoryCalls)
	require.Zero(t, dependencies.identityCalls)
	require.Len(t, service.calls, 1)
	call := service.calls[0]
	require.Equal(t, "usr_owner", call.UserID)
	require.Equal(t, "wsp_1", call.WorkspaceID)
	require.Equal(t, "agt_source", call.SourceAgentID)
	require.Equal(t, "config_only", call.Input.Mode)
	require.Equal(t, "mach_frozen", call.Input.MachineID)
	require.Equal(t, "codex", call.Input.Runtime)
	require.Equal(t, []string{"chn_1", "chn_2"}, call.Input.ChannelIDs)
	require.Empty(t, call.Input.Identity.User.ID)
	require.Empty(t, call.Input.Identity.SessionID)
	require.Empty(t, call.Input.Identity.InternalPurpose)
}

func TestOperationExecutorDuplicateWithResourcesUsesPersistedOwnerIdentity(t *testing.T) {
	persistedOwner := identity.User{
		ID: "usr_owner", Email: "owner@example.com", Name: "owner", DisplayName: "Persisted Owner",
	}
	service := &operationDuplicateServiceFake{result: agent.AppAgent{ID: "agt_copy"}}
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{{ID: "agt_source", Name: "source", DisplayName: "Source"}},
		owner:  persistedOwner,
	}
	executor := NewOperationExecutor(service,
		WithOperationAgentInventory(dependencies),
		WithOperationOwnerIdentityFinder(dependencies),
	)
	op, item := duplicateOperationFixture(t, agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 1, Mode: "with_resources",
	}, 0)

	_, err := executor.ExecuteItem(context.Background(), op, item)

	require.NoError(t, err)
	require.Equal(t, 1, dependencies.identityCalls)
	require.Len(t, service.calls, 1)
	identity := service.calls[0].Input.Identity
	require.Equal(t, persistedOwner, identity.User)
	require.Empty(t, identity.SessionID)
	require.Equal(t, "approved_agent_management_operation", identity.InternalPurpose)
}

func TestOperationExecutorDuplicateDependencyErrorsAreSafeAndDoNotMutate(t *testing.T) {
	proposal := agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 1, Mode: "with_resources",
	}
	for _, test := range []struct {
		name       string
		dependency *operationDuplicateDependenciesFake
		wantLookup int
	}{
		{
			name: "inventory",
			dependency: &operationDuplicateDependenciesFake{
				inventoryErr: errors.New("inventory leaked credential sk-inventory"),
			},
		},
		{
			name: "owner identity",
			dependency: &operationDuplicateDependenciesFake{
				agents:      []agent.AppAgent{{ID: "agt_source", Name: "source", DisplayName: "Source"}},
				identityErr: errors.New("identity leaked session sess-secret"),
			},
			wantLookup: 1,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			service := &operationDuplicateServiceFake{}
			executor := NewOperationExecutor(service,
				WithOperationAgentInventory(test.dependency),
				WithOperationOwnerIdentityFinder(test.dependency),
			)
			op, item := duplicateOperationFixture(t, proposal, 0)

			result, err := executor.ExecuteItem(context.Background(), op, item)

			require.Error(t, err)
			require.Nil(t, result)
			require.Equal(t, "agent operation duplicate failed", err.Error())
			require.Equal(t, test.wantLookup, test.dependency.identityCalls)
			require.Empty(t, service.calls)
		})
	}
}

func TestOperationExecutorDuplicateCountThreeUsesItemOrderAndDistinctNames(t *testing.T) {
	service := &operationDuplicateServiceFake{}
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{{ID: "agt_source", Name: "source", DisplayName: "Source"}},
	}
	executor := NewOperationExecutor(service, WithOperationAgentInventory(dependencies))
	proposal := agentapproval.DuplicateProposal{SourceAgentID: "agt_source", Count: 3}

	for index := 0; index < proposal.Count; index++ {
		service.result = agent.AppAgent{ID: "agt_copy_" + string(rune('1'+index))}
		op, item := duplicateOperationFixture(t, proposal, index)
		result, err := executor.ExecuteItem(context.Background(), op, item)
		require.NoError(t, err)
		require.JSONEq(t, `{"agentId":"`+service.result.ID+`"}`, string(result))
	}

	require.Len(t, service.calls, 3)
	require.Equal(t, []string{"source-copy-1", "source-copy-2", "source-copy-3"}, []string{
		service.calls[0].Input.Name, service.calls[1].Input.Name, service.calls[2].Input.Name,
	})
	require.Equal(t, []string{"Source copy 1", "Source copy 2", "Source copy 3"}, []string{
		service.calls[0].Input.DisplayName, service.calls[1].Input.DisplayName, service.calls[2].Input.DisplayName,
	})
}

func TestOperationExecutorDuplicateItemNameIsDeterministicWithoutPriorItems(t *testing.T) {
	service := &operationDuplicateServiceFake{result: agent.AppAgent{ID: "agt_copy_3"}}
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{{ID: "agt_source", Name: "source", DisplayName: "Source"}},
	}
	executor := NewOperationExecutor(service, WithOperationAgentInventory(dependencies))
	op, item := duplicateOperationFixture(t, agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 3,
	}, 2)

	_, err := executor.ExecuteItem(context.Background(), op, item)

	require.NoError(t, err)
	require.Len(t, service.calls, 1)
	require.Equal(t, "source-copy-3", service.calls[0].Input.Name)
	require.Equal(t, "Source copy 3", service.calls[0].Input.DisplayName)
}

func TestOperationExecutorDuplicateSkipsExistingNameOrDisplayNameCollisions(t *testing.T) {
	service := &operationDuplicateServiceFake{result: agent.AppAgent{ID: "agt_copy"}}
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{
			{ID: "agt_source", Name: "source", DisplayName: "Source"},
			{ID: "agt_existing_name", Name: "source-copy-1", DisplayName: "Unrelated"},
			{ID: "agt_existing_display", Name: "unrelated", DisplayName: "Source copy 1 2"},
		},
	}
	executor := NewOperationExecutor(service, WithOperationAgentInventory(dependencies))
	op, item := duplicateOperationFixture(t, agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 1,
	}, 0)

	_, err := executor.ExecuteItem(context.Background(), op, item)

	require.NoError(t, err)
	require.Len(t, service.calls, 1)
	require.Equal(t, "source-copy-1-3", service.calls[0].Input.Name)
	require.Equal(t, "Source copy 1 3", service.calls[0].Input.DisplayName)
}

func TestOperationExecutorDuplicateTruncatesUnicodeNamesByRune(t *testing.T) {
	baseName := strings.Repeat("界", 31)
	baseDisplayName := strings.Repeat("龙", 31)
	service := &operationDuplicateServiceFake{result: agent.AppAgent{ID: "agt_copy"}}
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{{ID: "agt_source", Name: baseName, DisplayName: baseDisplayName}},
	}
	executor := NewOperationExecutor(service, WithOperationAgentInventory(dependencies))
	op, item := duplicateOperationFixture(t, agentapproval.DuplicateProposal{
		SourceAgentID: "agt_source", Count: 1,
	}, 0)

	_, err := executor.ExecuteItem(context.Background(), op, item)

	require.NoError(t, err)
	require.Len(t, service.calls, 1)
	input := service.calls[0].Input
	require.LessOrEqual(t, utf8.RuneCountInString(input.Name), 32)
	require.LessOrEqual(t, utf8.RuneCountInString(input.DisplayName), 32)
	require.True(t, strings.HasSuffix(input.Name, "-copy-1"))
	require.True(t, strings.HasSuffix(input.DisplayName, " copy 1"))
	require.True(t, utf8.ValidString(input.Name))
	require.True(t, utf8.ValidString(input.DisplayName))
}

func TestOperationExecutorDuplicateReturnsSafeResultAndError(t *testing.T) {
	dependencies := &operationDuplicateDependenciesFake{
		agents: []agent.AppAgent{{ID: "agt_source", Name: "source", DisplayName: "Source"}},
	}
	proposal := agentapproval.DuplicateProposal{SourceAgentID: "agt_source", Count: 1}

	t.Run("result contains only agent id", func(t *testing.T) {
		service := &operationDuplicateServiceFake{result: agent.AppAgent{ID: "agt_copy"}}
		executor := NewOperationExecutor(service, WithOperationAgentInventory(dependencies))
		op, item := duplicateOperationFixture(t, proposal, 0)

		result, err := executor.ExecuteItem(context.Background(), op, item)

		require.NoError(t, err)
		require.JSONEq(t, `{"agentId":"agt_copy"}`, string(result))
		require.NotContains(t, strings.ToLower(string(result)), "session")
		require.NotContains(t, strings.ToLower(string(result)), "credential")
	})

	t.Run("service error is redacted", func(t *testing.T) {
		service := &operationDuplicateServiceFake{err: errors.New("credential sk-secret session raw-payload")}
		executor := NewOperationExecutor(service, WithOperationAgentInventory(dependencies))
		op, item := duplicateOperationFixture(t, proposal, 0)

		result, err := executor.ExecuteItem(context.Background(), op, item)

		require.Error(t, err)
		require.Nil(t, result)
		require.Equal(t, "agent operation duplicate failed", err.Error())
		require.Len(t, service.calls, 1)
	})
}

func duplicateOperationFixture(t *testing.T, proposal agentapproval.DuplicateProposal, itemIndex int) (*agentops.Operation, agentops.OperationItem) {
	t.Helper()
	payload := agentapproval.Payload{
		Action:    agentapproval.ActionDuplicate,
		Owner:     agentapproval.OwnerRef{UserID: "usr_owner"},
		Duplicate: &proposal,
	}
	op := operationFixture(t, payload)
	raw, err := json.Marshal(proposal)
	require.NoError(t, err)
	return op, agentops.OperationItem{ItemIndex: itemIndex, Input: raw}
}
