package agents

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
)

type fakeAgentStore struct {
	created             agent.AgentView
	createErr           error
	deleteErr           error
	getErr              error
	materializationErr  error
	createCalls         int
	deleteCalls         int
	getCalls            int
	materializationCall agent.AgentRoleMaterializationInput
	lastDeleteAgent     string
}

func (f *fakeAgentStore) CreateAgentForUser(_ context.Context, _, _, _ string, _ agent.CreateAgentParams) (agent.AgentView, error) {
	f.createCalls++
	if f.createErr != nil {
		return agent.AgentView{}, f.createErr
	}
	return f.created, nil
}

func (f *fakeAgentStore) DeleteAgentForWorkspace(_ context.Context, _, _, agentID string) (string, error) {
	f.deleteCalls++
	f.lastDeleteAgent = agentID
	return "mach_1", f.deleteErr
}

func (f *fakeAgentStore) GetAgentForUser(context.Context, string, string, string) (agent.AgentView, error) {
	f.getCalls++
	return agent.AgentView{AgentID: "agt_1", MachineID: "mach_1"}, f.getErr
}

func (f *fakeAgentStore) UpsertAgentRoleMaterialization(_ context.Context, input agent.AgentRoleMaterializationInput) error {
	f.materializationCall = input
	return f.materializationErr
}

type fakeRealizer struct {
	result          RealizationResult
	materialization agent.AgentRoleMaterialization
	impact          ResourceImpact
	counts          map[plan.MetricKey]int64
	err             error
	materializeErr  error
	impactErr       error
	countErr        error
	last            RealizeRoleForAgentInput
	lastMaterialize RealizeRoleForAgentInput
	impactCalls     int
	countCalls      int
	realizeCalls    int
}

func (f *fakeRealizer) RealizeRoleForAgent(_ context.Context, input RealizeRoleForAgentInput) (RealizationResult, error) {
	f.realizeCalls++
	f.last = input
	if f.err != nil {
		return RealizationResult{}, f.err
	}
	return f.result, nil
}

func (f *fakeRealizer) RoleMaterialization(_ context.Context, input RealizeRoleForAgentInput) (agent.AgentRoleMaterialization, error) {
	f.lastMaterialize = input
	if f.materializeErr != nil {
		return agent.AgentRoleMaterialization{}, f.materializeErr
	}
	if f.materialization.RoleID == "" {
		f.materialization = agent.AgentRoleMaterialization{
			RoleID:               f.result.RoleID,
			RoleVersionID:        "rlv_1",
			ActiveDefinitionPath: "memory/system/identity.md",
			ManifestItems: []agent.AgentRoleMaterializationItem{{
				Type:    "role_definition_asset",
				Name:    "memory/system/identity.md",
				Content: "# Role",
			}},
		}
	}
	return f.materialization, nil
}

func (f *fakeRealizer) RoleResourceImpact(_ context.Context, input RealizeRoleForAgentInput) (ResourceImpact, error) {
	f.impactCalls++
	f.last = input
	if f.impactErr != nil {
		return ResourceImpact{}, f.impactErr
	}
	return f.impact, nil
}

func (f *fakeRealizer) CountWorkspaceResource(_ context.Context, input RealizeRoleForAgentInput, metric plan.MetricKey, _ plan.QuotaScope) (int64, error) {
	f.countCalls++
	f.last = input
	if f.countErr != nil {
		return 0, f.countErr
	}
	return f.counts[metric], nil
}

type fakeWorkspaceResourceLimitStore struct {
	limits    map[plan.MetricKey]plan.WorkspaceResourceLimit
	calls     int
	workspace string
	owner     string
}

func (f *fakeWorkspaceResourceLimitStore) LoadWorkspaceResourceLimit(_ context.Context, workspaceID string, ownerUserID string, metric plan.MetricKey) (plan.WorkspaceResourceLimit, error) {
	f.calls++
	f.workspace = workspaceID
	f.owner = ownerUserID
	limit := f.limits[metric]
	if limit.OrganizationID == "" {
		limit.OrganizationID = "org_1"
	}
	if limit.PlanKey == "" {
		limit.PlanKey = plan.PlanFree
	}
	if limit.PlanName == "" {
		limit.PlanName = "Free"
	}
	if limit.Metric == "" {
		limit.Metric = metric
	}
	if limit.Scope == "" {
		limit.Scope = plan.QuotaScopeWorkspaceMember
	}
	return limit, nil
}

func TestCreateAgentFromRoleSaga(t *testing.T) {
	input := CreateFromRoleInput{
		UserID:      "usr_1",
		WorkspaceID: "wsp_1",
		MachineID:   "mach_1",
		Agent:       agent.CreateAgentParams{Name: "research-agent", DisplayName: "Research Agent", SourceKind: "preset", SourceRef: "preset:research-agent"},
		Source:      Source{Kind: "preset", PresetID: "research-agent", Locale: "en-US"},
		Realization: RealizationOptions{Apply: true, SkipOptional: true, StorageScope: "workspace", TemplateLocale: "en-US"},
	}

	t.Run("success", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_1", MachineID: "mach_1"}}
		realizer := &fakeRealizer{result: RealizationResult{RoleID: "role_1", AgentID: "agt_1", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}}}}
		svc := NewService(agents, realizer)

		result, err := svc.CreateAgentFromRole(context.Background(), input)
		require.NoError(t, err)
		require.False(t, result.Partial)
		require.Equal(t, "agt_1", result.Created.AgentID)
		require.Equal(t, "role_1", result.Realization.RoleID)
		require.Equal(t, "agt_1", realizer.last.AgentID)
		require.Equal(t, "usr_1", realizer.last.UserID)
		require.Equal(t, input.Source, realizer.last.Source)
		require.Equal(t, input.Realization, realizer.last.Realization)
		require.NotEmpty(t, result.AgentRoleBindingID)
		require.Equal(t, result.AgentRoleBindingID, agents.materializationCall.BindingID)
		require.Equal(t, "role_1", agents.materializationCall.RoleID)
		require.Equal(t, "rlv_1", agents.materializationCall.RoleVersionID)
		require.Zero(t, agents.deleteCalls)
	})

	t.Run("required failure rolls back", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_2", MachineID: "mach_1"}}
		realizer := &fakeRealizer{result: RealizationResult{RoleID: "role_2", AgentID: "agt_2", Items: []RealizationItem{{ManifestItemType: "knowledge_base", Status: "unresolved", Required: true, Message: "permission denied"}}}}
		svc := NewService(agents, realizer)

		_, err := svc.CreateAgentFromRole(context.Background(), input)
		require.Error(t, err)
		var realizationErr *RealizationError
		require.ErrorAs(t, err, &realizationErr)
		require.Equal(t, 1, agents.deleteCalls)
		require.Equal(t, "agt_2", agents.lastDeleteAgent)
		require.Equal(t, "role_2", realizationErr.Result.RoleID)
	})

	t.Run("optional failure returns partial success", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_3", MachineID: "mach_1"}}
		realizer := &fakeRealizer{result: RealizationResult{RoleID: "role_3", AgentID: "agt_3", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}, {ManifestItemType: "knowledge_base", Status: "skipped", Required: false, Message: "skipped"}}}}
		svc := NewService(agents, realizer)

		result, err := svc.CreateAgentFromRole(context.Background(), input)
		require.NoError(t, err)
		require.True(t, result.Partial)
		require.Zero(t, agents.deleteCalls)
	})

	t.Run("cleanup failure is acked with detail", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_4", MachineID: "mach_1"}, deleteErr: errors.New("delete failed")}
		realizer := &fakeRealizer{result: RealizationResult{RoleID: "role_4", AgentID: "agt_4", Items: []RealizationItem{{ManifestItemType: "memory", Status: "unresolved", Required: true}}}}
		svc := NewService(agents, realizer)

		_, err := svc.CreateAgentFromRole(context.Background(), input)
		require.Error(t, err)
		var realizationErr *RealizationError
		require.ErrorAs(t, err, &realizationErr)
		require.EqualError(t, realizationErr.CleanupErr, "delete failed")
		require.Contains(t, err.Error(), "rollback cleanup failed")
	})

	t.Run("materialization failure rolls back", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_5", MachineID: "mach_1"}}
		realizer := &fakeRealizer{
			result:         RealizationResult{RoleID: "role_5", AgentID: "agt_5", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}}},
			materializeErr: errors.New("materialization unavailable"),
		}
		svc := NewService(agents, realizer)

		_, err := svc.CreateAgentFromRole(context.Background(), input)

		require.Error(t, err)
		var realizationErr *RealizationError
		require.ErrorAs(t, err, &realizationErr)
		require.EqualError(t, realizationErr.Cause, "materialization unavailable")
		require.Equal(t, 1, agents.deleteCalls)
		require.Equal(t, "agt_5", agents.lastDeleteAgent)
	})

	t.Run("requested version mismatch rolls back", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_6", MachineID: "mach_1"}}
		realizer := &fakeRealizer{
			result:          RealizationResult{RoleID: "role_6", AgentID: "agt_6", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}}},
			materialization: agent.AgentRoleMaterialization{RoleID: "role_6", RoleVersionID: "rv_actual", ActiveDefinitionPath: "memory/system/identity.md"},
		}
		svc := NewService(agents, realizer)
		versionedInput := input
		versionedInput.Source = Source{Kind: "my-role", RoleID: "role_6", VersionID: "rv_requested"}

		_, err := svc.CreateAgentFromRole(context.Background(), versionedInput)

		require.Error(t, err)
		var realizationErr *RealizationError
		require.ErrorAs(t, err, &realizationErr)
		require.Contains(t, realizationErr.Cause.Error(), "rv_requested")
		require.Equal(t, 1, agents.deleteCalls)
		require.Equal(t, "agt_6", agents.lastDeleteAgent)
		require.Equal(t, "rv_requested", realizer.lastMaterialize.Source.VersionID)
	})
}

func TestCreateAgentFromRoleQuotaGate(t *testing.T) {
	input := CreateFromRoleInput{
		UserID:      "usr_1",
		WorkspaceID: "wsp_1",
		MachineID:   "mach_1",
		Agent:       agent.CreateAgentParams{Name: "research-agent", DisplayName: "Research Agent", SourceKind: "preset", SourceRef: "preset:research-agent"},
		Source:      Source{Kind: "preset", PresetID: "research-agent", Locale: "en-US"},
		Realization: RealizationOptions{Apply: true, StorageScope: "workspace"},
	}

	t.Run("billing disabled skips quota planning", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_1", MachineID: "mach_1"}}
		realizer := &fakeRealizer{
			impact: ResourceImpact{ManagedMemories: 1},
			result: RealizationResult{RoleID: "role_1", AgentID: "agt_1", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}}},
		}
		quotaStore := &fakeWorkspaceResourceLimitStore{limits: map[plan.MetricKey]plan.WorkspaceResourceLimit{
			plan.MetricManagedMemories: {Limit: 0},
		}}
		svc := NewService(agents, realizer, ServiceOptions{BillingEnabled: false, QuotaStore: quotaStore})

		_, err := svc.CreateAgentFromRole(context.Background(), input)

		require.NoError(t, err)
		require.Zero(t, realizer.impactCalls)
		require.Zero(t, realizer.countCalls)
		require.Zero(t, quotaStore.calls)
	})

	t.Run("billing enabled rolls back before realization when quota is exceeded", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_2", MachineID: "mach_1"}}
		realizer := &fakeRealizer{
			impact: ResourceImpact{ManagedMemories: 1},
			counts: map[plan.MetricKey]int64{plan.MetricManagedMemories: 10},
			result: RealizationResult{RoleID: "role_2", AgentID: "agt_2", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}}},
		}
		quotaStore := &fakeWorkspaceResourceLimitStore{limits: map[plan.MetricKey]plan.WorkspaceResourceLimit{
			plan.MetricManagedMemories: {Limit: 10},
		}}
		svc := NewService(agents, realizer, ServiceOptions{BillingEnabled: true, QuotaStore: quotaStore})

		_, err := svc.CreateAgentFromRole(context.Background(), input)

		require.Error(t, err)
		var realizationErr *RealizationError
		require.ErrorAs(t, err, &realizationErr)
		var quotaErr plan.QuotaExceededError
		require.ErrorAs(t, realizationErr.Cause, &quotaErr)
		require.Equal(t, plan.MetricManagedMemories, quotaErr.Metric)
		require.Equal(t, int64(10), quotaErr.Current)
		require.Equal(t, int64(1), quotaErr.Delta)
		require.Equal(t, 1, agents.deleteCalls)
		require.Equal(t, "agt_2", agents.lastDeleteAgent)
		require.Equal(t, 1, realizer.impactCalls)
		require.Equal(t, 1, realizer.countCalls)
		require.Zero(t, realizer.realizeCalls)
		require.Empty(t, realizer.lastMaterialize.AgentID)
		require.Equal(t, 1, quotaStore.calls)
		require.Equal(t, "wsp_1", quotaStore.workspace)
		require.Equal(t, "usr_1", quotaStore.owner)
	})

	t.Run("unlimited plan skips current usage count", func(t *testing.T) {
		agents := &fakeAgentStore{created: agent.AgentView{AgentID: "agt_3", MachineID: "mach_1"}}
		realizer := &fakeRealizer{
			impact: ResourceImpact{ManagedMemories: 1},
			result: RealizationResult{RoleID: "role_3", AgentID: "agt_3", Items: []RealizationItem{{ManifestItemType: "memory", Status: "realized", Required: true}}},
		}
		quotaStore := &fakeWorkspaceResourceLimitStore{limits: map[plan.MetricKey]plan.WorkspaceResourceLimit{
			plan.MetricManagedMemories: {Limit: -1},
		}}
		svc := NewService(agents, realizer, ServiceOptions{BillingEnabled: true, QuotaStore: quotaStore})

		_, err := svc.CreateAgentFromRole(context.Background(), input)

		require.NoError(t, err)
		require.Equal(t, 1, realizer.impactCalls)
		require.Zero(t, realizer.countCalls)
		require.Equal(t, 1, realizer.realizeCalls)
		require.Equal(t, 1, quotaStore.calls)
	})
}

func TestNewServiceRequiresQuotaDependenciesWhenBillingEnabled(t *testing.T) {
	agents := &fakeAgentStore{}
	realizer := struct {
		RoleRealizer
	}{RoleRealizer: &fakeRealizer{}}

	require.Nil(t, NewService(agents, realizer, ServiceOptions{BillingEnabled: true}))
}

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

	agents := &fakeAgentStore{}
	realizer := &fakeRealizer{
		result: RealizationResult{RoleID: "role_1", AgentID: "agt_1", Apply: true, Items: []RealizationItem{{ManifestItemType: "role_definition_asset", Status: "realized", Required: true}}},
		materialization: agent.AgentRoleMaterialization{
			RoleID:               "role_1",
			RoleVersionID:        "rlv_1",
			ActiveDefinitionPath: "memory/system/identity.md",
			ManifestItems:        []agent.AgentRoleMaterializationItem{{Type: "role_definition_asset", Name: "IDENTITY.md", Content: "# Reviewer"}},
		},
	}
	svc := NewService(agents, realizer)

	result, err := svc.ApplyRoleToAgent(context.Background(), RealizeRoleForAgentInput{
		UserID:      "usr_1",
		WorkspaceID: "wsp_1",
		AgentID:     "agt_1",
		Source:      Source{Kind: "my-role", RoleID: "role_1", VersionID: "rlv_1"},
		Realization: RealizationOptions{Apply: true},
	})
	require.NoError(t, err)
	require.False(t, result.Partial)
	require.Zero(t, agents.createCalls)
	require.Equal(t, 1, agents.getCalls)
	require.Equal(t, "role_1", realizer.last.Source.RoleID)
	require.Equal(t, "role_1", realizer.lastMaterialize.Source.RoleID)
	require.Equal(t, "agt_1", agents.materializationCall.AgentID)
	require.Equal(t, "role_1", agents.materializationCall.RoleID)
	require.Equal(t, "rlv_1", agents.materializationCall.RoleVersionID)
	require.Equal(t, "# Reviewer", agents.materializationCall.ManifestItems[0].Content)
}

func TestValidateSource(t *testing.T) {
	svc := NewService(&fakeAgentStore{}, &fakeRealizer{})
	_, err := svc.CreateAgentFromRole(context.Background(), CreateFromRoleInput{Source: Source{Kind: "my-role", RoleID: "role_1", VersionID: ""}})
	var validationErr *ValidationError
	require.ErrorAs(t, err, &validationErr)
}
