package webagent

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	"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/organization"
)

type operationAgentServiceFake struct {
	createCalls []CreateAgentInput
	updateCalls []UpdateAgentInput
	deleteCalls []string
	result      agent.AppAgent
	err         error
}

func (fake *operationAgentServiceFake) CreateAgent(_ context.Context, _, _ string, input CreateAgentInput) (agent.AppAgent, error) {
	fake.createCalls = append(fake.createCalls, input)
	return fake.result, fake.err
}

func (fake *operationAgentServiceFake) UpdateAgent(_ context.Context, _, _ string, input UpdateAgentInput) (agent.AppAgent, error) {
	fake.updateCalls = append(fake.updateCalls, input)
	return fake.result, fake.err
}

func (fake *operationAgentServiceFake) DeleteAgent(_ context.Context, _, _, agentID string) error {
	fake.deleteCalls = append(fake.deleteCalls, agentID)
	return fake.err
}

func TestOperationExecutorRejectsFrozenIdentityMismatchBeforeServiceCall(t *testing.T) {
	for _, test := range []struct {
		name   string
		action string
		owner  string
	}{
		{name: "action", action: agentapproval.ActionDelete, owner: "usr_owner"},
		{name: "owner", action: agentapproval.ActionCreate, owner: "usr_other"},
	} {
		t.Run(test.name, func(t *testing.T) {
			fake := &operationAgentServiceFake{}
			executor := NewOperationExecutor(fake)
			op, item := createOperationFixture(t, agentapproval.Payload{
				Action: agentapproval.ActionCreate,
				Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
				Create: &agentapproval.CreateProposal{Name: "agent", MachineID: "mach_1", Runtime: "codex"},
			})
			op.Action, op.OwnerUserID = test.action, test.owner

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

			require.Error(t, err)
			require.Empty(t, fake.createCalls)
			require.Empty(t, fake.updateCalls)
			require.Empty(t, fake.deleteCalls)
		})
	}
}

func TestOperationExecutorRejectsInvalidSingletonItemIndexBeforeServiceCall(t *testing.T) {
	for _, action := range []string{agentapproval.ActionCreate, agentapproval.ActionUpdate} {
		for _, itemIndex := range []int{-1, 1} {
			t.Run(fmt.Sprintf("%s/index_%d", action, itemIndex), func(t *testing.T) {
				fake := &operationAgentServiceFake{}
				executor := NewOperationExecutor(fake)

				var op *agentops.Operation
				var item agentops.OperationItem
				switch action {
				case agentapproval.ActionCreate:
					op, item = createOperationFixture(t, agentapproval.Payload{
						Action: agentapproval.ActionCreate,
						Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
						Create: &agentapproval.CreateProposal{Name: "agent", MachineID: "mach_1", Runtime: "codex"},
					})
				case agentapproval.ActionUpdate:
					description := "updated"
					op, item = updateOperationFixture(t, agentapproval.UpdateProposal{AgentID: "agt_1", Description: &description})
				}
				item.ItemIndex = itemIndex

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

				require.Error(t, err)
				require.Empty(t, fake.createCalls)
				require.Empty(t, fake.updateCalls)
				require.Empty(t, fake.deleteCalls)
			})
		}
	}
}

func TestOperationExecutorCreateUsesTrustedBindingSelection(t *testing.T) {
	for _, test := range []struct {
		name      string
		model     string
		profileID string
		wantMode  string
	}{
		{name: "server-side profile local", model: "ignored", profileID: "prof_1", wantMode: agent.AgentLLMModeLocal},
		{name: "agentteam hosted", model: "agentteam/deepseek-v4-pro", wantMode: agent.AgentLLMModeAgentTeamHosted},
		{name: "workspace proxy", model: "gpt-5.4", wantMode: agent.AgentLLMModeAgentTeamProxy},
		{name: "empty model uses workspace proxy", wantMode: agent.AgentLLMModeAgentTeamProxy},
	} {
		t.Run(test.name, func(t *testing.T) {
			fake := &operationAgentServiceFake{result: agent.AppAgent{ID: "agt_new"}}
			executor := NewOperationExecutor(fake)
			op, item := createOperationFixture(t, agentapproval.Payload{
				Action: agentapproval.ActionCreate,
				Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
				Create: &agentapproval.CreateProposal{
					Name: "agent", MachineID: "mach_1", Runtime: "codex",
					Model: test.model, ModelProfileID: test.profileID,
				},
			})

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

			require.NoError(t, err)
			require.JSONEq(t, `{"agentId":"agt_new"}`, string(result))
			require.Len(t, fake.createCalls, 1)
			input := fake.createCalls[0]
			require.Equal(t, test.profileID, input.ModelProfileID)
			require.Equal(t, test.wantMode, input.LLMMode)
			require.Empty(t, input.BaseURL)
			require.Empty(t, input.APIKey)
			require.Empty(t, input.Headers)
			require.Empty(t, input.WireAPI)
		})
	}
}

func TestOperationExecutorCreateResolvesTrustedBindingsThroughAgentService(t *testing.T) {
	for _, test := range []struct {
		name            string
		model           string
		profileID       string
		profile         agent.ModelProfile
		wantMode        string
		wantModel       string
		wantBaseURL     string
		wantAPIKey      string
		wantWireAPI     string
		wantProxyCalls  int
		wantHostedCalls int
	}{
		{
			name: "profile is loaded server side", model: "untrusted-model", profileID: "prof_1",
			profile: agent.ModelProfile{
				ID: "prof_1", Model: "profile-model", BaseURL: "https://profile.example",
				APIKey: "server-profile-key", WireAPI: "anthropic",
			},
			wantMode: agent.AgentLLMModeLocal, wantModel: "profile-model",
			wantBaseURL: "https://profile.example", wantAPIKey: "server-profile-key", wantWireAPI: "anthropic",
		},
		{
			name: "agentteam model uses hosted binding", model: "agentteam/deepseek-v4-pro",
			wantMode: agent.AgentLLMModeAgentTeamHosted, wantModel: "agentteam/deepseek-v4-pro", wantHostedCalls: 1,
		},
		{
			name: "other model uses workspace proxy", model: "gpt-5.4",
			wantMode: agent.AgentLLMModeAgentTeamProxy, wantModel: "gpt-5.4",
			wantAPIKey: "server-workspace-proxy-key", wantProxyCalls: 1,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			store := &fakeStore{
				createdAgent: agent.AgentView{AgentID: "agt_new"},
				modelProfile: test.profile,
			}
			orgs := &recordingOperationOrganizationProvider{proxyKey: "server-workspace-proxy-key"}
			hosted := &fakeHostedAgentValidator{}
			service := NewAgentService(store, nil, nil, nil, nil, nil, orgs, hosted, nil, false, store, nil)
			executor := NewOperationExecutor(service)
			op, item := createOperationFixture(t, agentapproval.Payload{
				Action: agentapproval.ActionCreate,
				Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
				Create: &agentapproval.CreateProposal{
					Name: "agent", MachineID: "mach_1", Runtime: "codex",
					Model: test.model, ModelProfileID: test.profileID,
				},
			})

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

			require.NoError(t, err)
			require.JSONEq(t, `{"agentId":"agt_new"}`, string(result))
			params := store.lastCreate.Params
			require.Equal(t, test.wantMode, params.LLMMode)
			require.Equal(t, test.wantModel, params.Model)
			require.Equal(t, test.wantBaseURL, params.BaseURL)
			require.Equal(t, test.wantAPIKey, params.APIKey)
			require.Equal(t, test.wantWireAPI, params.WireAPI)
			require.Equal(t, test.wantProxyCalls, orgs.proxyCalls)
			require.Equal(t, test.wantHostedCalls, hosted.calls)
		})
	}
}

func TestOperationExecutorUpdatePreservesOptionalStringsAndConditionallyRebinds(t *testing.T) {
	clear := ""
	description := "new description"
	model := "gpt-5.4"
	profile := "prof_1"
	for _, test := range []struct {
		name              string
		proposal          agentapproval.UpdateProposal
		wantDescription   dto.OptionalString
		wantModel         dto.OptionalString
		wantProfile       dto.OptionalString
		wantTrustedRebind bool
	}{
		{
			name: "description value does not rebind", proposal: agentapproval.UpdateProposal{AgentID: "agt_1", Description: &description},
			wantDescription: optionalString(&description),
		},
		{
			name: "description clear does not rebind", proposal: agentapproval.UpdateProposal{AgentID: "agt_1", Description: &clear},
			wantDescription: optionalString(&clear),
		},
		{
			name: "model value rebinds", proposal: agentapproval.UpdateProposal{AgentID: "agt_1", Model: &model},
			wantModel: optionalString(&model), wantTrustedRebind: true,
		},
		{
			name: "model clear rebinds", proposal: agentapproval.UpdateProposal{AgentID: "agt_1", Model: &clear},
			wantModel: optionalString(&clear), wantTrustedRebind: true,
		},
		{
			name: "profile value rebinds", proposal: agentapproval.UpdateProposal{AgentID: "agt_1", ModelProfileID: &profile},
			wantProfile: optionalString(&profile), wantTrustedRebind: true,
		},
		{
			name: "profile clear rebinds", proposal: agentapproval.UpdateProposal{AgentID: "agt_1", ModelProfileID: &clear},
			wantProfile: optionalString(&clear), wantTrustedRebind: true,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			fake := &operationAgentServiceFake{result: agent.AppAgent{ID: "agt_1"}}
			executor := NewOperationExecutor(fake)
			op, item := updateOperationFixture(t, test.proposal)

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

			require.NoError(t, err)
			require.Len(t, fake.updateCalls, 1)
			input := fake.updateCalls[0]
			require.Equal(t, test.wantDescription, input.Description)
			require.Equal(t, test.wantModel, input.Model)
			require.Equal(t, test.wantProfile, input.ModelProfileID)
			require.Equal(t, test.wantTrustedRebind, input.ResolveTrustedLLM)
			require.False(t, input.LLMMode.Set)
			require.False(t, input.BaseURL.Set)
			require.False(t, input.APIKey.Set)
			require.False(t, input.Headers.Set)
			require.False(t, input.WireAPI.Set)
		})
	}
}

func TestOperationExecutorUpdateOnlyResolvesTrustedBindingWhenTouched(t *testing.T) {
	description := "metadata only"
	model := "gpt-5.4"
	hostedModel := "agentteam/deepseek-v4-pro"
	profileID := "prof_1"
	for _, test := range []struct {
		name            string
		proposal        agentapproval.UpdateProposal
		profile         agent.ModelProfile
		wantLLMSet      bool
		wantMode        string
		wantAPIKey      string
		wantProxyCalls  int
		wantHostedCalls int
	}{
		{
			name:     "description only preserves existing binding",
			proposal: agentapproval.UpdateProposal{AgentID: "agt_1", Description: &description},
		},
		{
			name:       "model uses server workspace proxy",
			proposal:   agentapproval.UpdateProposal{AgentID: "agt_1", Model: &model},
			wantLLMSet: true, wantMode: agent.AgentLLMModeAgentTeamProxy,
			wantAPIKey: "server-workspace-proxy-key", wantProxyCalls: 1,
		},
		{
			name:       "agentteam model uses server hosted binding",
			proposal:   agentapproval.UpdateProposal{AgentID: "agt_1", Model: &hostedModel},
			wantLLMSet: true, wantMode: agent.AgentLLMModeAgentTeamHosted, wantHostedCalls: 1,
		},
		{
			name:     "profile uses server profile credential",
			proposal: agentapproval.UpdateProposal{AgentID: "agt_1", ModelProfileID: &profileID},
			profile: agent.ModelProfile{
				ID: "prof_1", Model: "profile-model", BaseURL: "https://profile.example",
				APIKey: "server-profile-key", WireAPI: "anthropic",
			},
			wantLLMSet: true, wantMode: agent.AgentLLMModeLocal, wantAPIKey: "server-profile-key",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			store := &fakeStore{
				agentView: agent.AgentView{
					AgentID: "agt_1", MachineID: "mach_1", Runtime: "codex", Model: "old-model",
					LLMMode: agent.AgentLLMModeAgentTeamProxy, APIKey: "existing-server-key",
				},
				modelProfile: test.profile,
			}
			orgs := &recordingOperationOrganizationProvider{proxyKey: "server-workspace-proxy-key"}
			hosted := &fakeHostedAgentValidator{}
			service := NewAgentService(store, nil, nil, nil, nil, nil, orgs, hosted, nil, false, store, nil)
			executor := NewOperationExecutor(service)
			op, item := updateOperationFixture(t, test.proposal)

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

			require.NoError(t, err)
			require.Equal(t, test.wantLLMSet, store.lastUpdate.LLMSet)
			require.Equal(t, test.wantProxyCalls, orgs.proxyCalls)
			require.Equal(t, test.wantHostedCalls, hosted.calls)
			if test.wantLLMSet {
				require.NotNil(t, store.lastUpdate.LLMMode)
				require.Equal(t, test.wantMode, *store.lastUpdate.LLMMode)
				require.NotNil(t, store.lastUpdate.APIKey)
				require.Equal(t, test.wantAPIKey, *store.lastUpdate.APIKey)
			} else {
				require.Nil(t, store.lastUpdate.LLMMode)
				require.Nil(t, store.lastUpdate.APIKey)
			}
		})
	}
}

func TestOperationExecutorDeleteCallsServiceExactlyOnceAndReturnsSafeResult(t *testing.T) {
	fake := &operationAgentServiceFake{}
	executor := NewOperationExecutor(fake)
	payload := agentapproval.Payload{
		Action: agentapproval.ActionDelete,
		Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
		Delete: &agentapproval.DeleteProposal{AgentIDs: []string{"agt_1"}},
	}
	op := operationFixture(t, payload)
	item := agentops.OperationItem{Input: json.RawMessage(`{"agentId":"agt_1"}`)}

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

	require.NoError(t, err)
	require.Equal(t, []string{"agt_1"}, fake.deleteCalls)
	require.JSONEq(t, `{"agentId":"agt_1"}`, string(result))
	require.NotContains(t, strings.ToLower(string(result)), "key")
	require.NotContains(t, strings.ToLower(string(result)), "credential")
}

func TestOperationExecutorErrorsDoNotExposeCredentialBearingInputs(t *testing.T) {
	secret := "sk-secret"
	fake := &operationAgentServiceFake{err: errors.New("upstream rejected apiKey " + secret)}
	executor := NewOperationExecutor(fake)
	op, item := createOperationFixture(t, agentapproval.Payload{
		Action: agentapproval.ActionCreate,
		Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
		Create: &agentapproval.CreateProposal{Name: "agent", MachineID: "mach_1", Runtime: "codex"},
	})

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

	require.Error(t, err)
	require.Nil(t, result)
	require.NotContains(t, err.Error(), secret)
	require.NotContains(t, strings.ToLower(err.Error()), "apikey")
}

func TestOperationExecutorRejectsRawCredentialsBeforeServiceCall(t *testing.T) {
	fake := &operationAgentServiceFake{}
	executor := NewOperationExecutor(fake)
	op := operationFixture(t, agentapproval.Payload{
		Action: agentapproval.ActionCreate,
		Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
		Create: &agentapproval.CreateProposal{Name: "agent", MachineID: "mach_1", Runtime: "codex"},
	})
	item := agentops.OperationItem{Input: json.RawMessage(`{"name":"agent","machineId":"mach_1","runtime":"codex","apiKey":"sk-raw","baseUrl":"https://raw.example","headers":{"Authorization":"secret"},"session":"raw"}`)}

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

	require.Error(t, err)
	require.Nil(t, result)
	require.Empty(t, fake.createCalls)
}

func optionalString(value *string) dto.OptionalString {
	return dto.OptionalString{Set: true, Value: value}
}

func createOperationFixture(t *testing.T, payload agentapproval.Payload) (*agentops.Operation, agentops.OperationItem) {
	t.Helper()
	op := operationFixture(t, payload)
	raw, err := json.Marshal(payload.Create)
	require.NoError(t, err)
	return op, agentops.OperationItem{Input: raw}
}

func updateOperationFixture(t *testing.T, proposal agentapproval.UpdateProposal) (*agentops.Operation, agentops.OperationItem) {
	t.Helper()
	payload := agentapproval.Payload{
		Action: agentapproval.ActionUpdate,
		Owner:  agentapproval.OwnerRef{UserID: "usr_owner"},
		Update: &proposal,
	}
	op := operationFixture(t, payload)
	raw, err := json.Marshal(proposal)
	require.NoError(t, err)
	return op, agentops.OperationItem{Input: raw}
}

func operationFixture(t *testing.T, payload agentapproval.Payload) *agentops.Operation {
	t.Helper()
	raw, err := json.Marshal(payload)
	require.NoError(t, err)
	return &agentops.Operation{
		WorkspaceID: "wsp_1", OwnerUserID: "usr_owner", Action: payload.Action, Payload: raw,
		RequestID: "req_1", ApprovalID: "apr_1",
	}
}

type recordingOperationOrganizationProvider struct {
	proxyKey   string
	proxyCalls int
}

func (provider *recordingOperationOrganizationProvider) ListVisibleOrganizationProvidersForWorkspace(context.Context, string) ([]organization.OrganizationProviderView, error) {
	return nil, nil
}

func (provider *recordingOperationOrganizationProvider) CreateInternalWorkspaceProxyKey(context.Context, string, time.Time) (string, error) {
	provider.proxyCalls++
	return provider.proxyKey, nil
}
