package main

import (
	"bytes"
	"context"
	"log/slog"
	"os"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/im"
	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/router"
	"github.com/tidbcloud/AgentTeam/internal/service/approvalapp"
	"github.com/tidbcloud/AgentTeam/internal/service/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

func TestConfiguredE2ETiDBAdminDSN(t *testing.T) {
	t.Run("prefers explicit e2e admin dsn", func(t *testing.T) {
		t.Setenv("AGENTTEAM_E2E_TIDB_ADMIN_DSN", "admin-e2e")
		t.Setenv("AGENTTEAM_TIDB_TEST_ADMIN_DSN", "admin-test")

		require.Equal(t, "admin-e2e", configuredE2ETiDBAdminDSN())
	})

	t.Run("falls back to tidb test admin dsn", func(t *testing.T) {
		t.Setenv("AGENTTEAM_E2E_TIDB_ADMIN_DSN", "")
		t.Setenv("AGENTTEAM_TIDB_TEST_ADMIN_DSN", "admin-test")

		require.Equal(t, "admin-test", configuredE2ETiDBAdminDSN())
	})
}

func TestE2ETiDBDatabaseDSN(t *testing.T) {
	dsn, err := e2eTiDBDatabaseDSN(
		"root@tcp(127.0.0.1:4000)/?multiStatements=true&parseTime=true&loc=UTC",
		"agentteam_e2e_test",
	)
	require.NoError(t, err)
	require.Contains(t, dsn, "/agentteam_e2e_test?")
	require.Contains(t, dsn, "multiStatements=true")
	require.Contains(t, dsn, "parseTime=true")
}

func TestConfiguredE2ETiDBDSN(t *testing.T) {
	t.Run("prefers explicit e2e dsn", func(t *testing.T) {
		t.Setenv("AGENTTEAM_E2E_TIDB_DSN", "dsn-e2e")
		t.Setenv("AGENTTEAM_TIDB_TEST_DSN", "dsn-test")
		t.Setenv("AGENTTEAM_DATABASE_DSN", "dsn-core")

		require.Equal(t, "dsn-e2e", configuredE2ETiDBDSN())
	})

	t.Run("falls back to tidb test dsn", func(t *testing.T) {
		t.Setenv("AGENTTEAM_E2E_TIDB_DSN", "")
		t.Setenv("AGENTTEAM_TIDB_TEST_DSN", "dsn-test")
		t.Setenv("AGENTTEAM_DATABASE_DSN", "dsn-core")

		require.Equal(t, "dsn-test", configuredE2ETiDBDSN())
	})

	t.Run("ignores core storage dsn", func(t *testing.T) {
		t.Setenv("AGENTTEAM_E2E_TIDB_DSN", "")
		t.Setenv("AGENTTEAM_TIDB_TEST_DSN", "")
		t.Setenv("AGENTTEAM_DATABASE_DSN", "dsn-core")

		require.Empty(t, configuredE2ETiDBDSN())
	})
}

func TestE2EStoreOptionsConfigureAppCredentialStorage(t *testing.T) {
	options := e2eStoreOptions("")

	require.Equal(t, e2eAppCredentialStorageKey, options.AppCredentialStorageKey)
	require.Len(t, options.AppCredentialStorageKey, 32)
}

func TestE2EIMDependenciesWireVisibleScopesForRouterValidation(t *testing.T) {
	dependencies := router.Dependencies{
		API: router.APIDependencies{
			IM: im.Dependencies{
				VisibleScopes: message.NewSQLVisibleScopeAuthorizer(fakeVisibleScopeQueryer{}),
			},
		},
	}

	err := dependencies.ValidateForProfile(router.ProfileWorkspace)

	require.Error(t, err)
	require.NotContains(t, err.Error(), "API.IM.VisibleScopes")
}

func TestE2EExtensionRouteDependenciesAreComplete(t *testing.T) {
	dependencies := e2eExtensionRouteDependencies(nil)

	require.NotNil(t, dependencies.Registry)
	require.NotNil(t, dependencies.Invoker)

	routerDependencies := router.Dependencies{Extension: dependencies}
	err := routerDependencies.ValidateForProfile(router.ProfileAll)
	require.Error(t, err)
	require.NotContains(t, err.Error(), "Extension.Registry")
	require.NotContains(t, err.Error(), "Extension.Invoker")
}

func TestE2EServerWiresExtensionRuntime(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)

	require.Contains(
		t,
		string(source),
		"Extension: e2eExtensionRouteDependencies(corePool),",
	)
}

func TestE2EIMDependenciesWireChannelAccessForSnapshotAdmission(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)

	require.Contains(t, string(source), "ChannelAccess:             stores.Channel,")
}

func TestE2EServiceAccountRouteDependenciesAreComplete(t *testing.T) {
	stores := &store.Graph{
		Identity:  &identity.Store{},
		Workspace: &workspace.Store{},
	}
	dependencies := e2eServiceAccountRouteDependencies(
		stores.Identity,
		stores.Workspace,
		serviceaccount.New(nil),
		approvalapp.NewProjector(nil, nil),
		true,
	)

	require.NotNil(t, dependencies.SessionAuth)
	require.NotNil(t, dependencies.WorkspaceStore)
	require.NotNil(t, dependencies.Service)
	require.NotNil(t, dependencies.PendingRequestProjector)
	require.True(t, dependencies.EmailVerificationRequired)
}

func TestE2EServerWiresApprovalSubjectsAndWorkers(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)
	main := string(source)

	require.Contains(t, main, "approvalSubjects := approvalapp.NewSubjectRegistry(stores.Approval)")
	require.Contains(t, main, "approvalapp.NewTeamRoleProposalSubjectHandler(")
	require.Contains(t, main, "collaborator.NewWithStore(corePool, stores.Approval, taskStore)")
	require.Contains(t, main, "approvalapp.NewCollaboratorRequestSubjectHandler(")
	require.Contains(t, main, "CollaboratorRequests:        collaboratorRequestService,")
	require.Contains(t, main, "agentDeps.ApprovalSubjects = approvalSubjects")
	require.Contains(t, main, "internalAppHandlers.Register(approvalapp.HandlerKey")
	require.Contains(t, main, "approvalProjectionReconciler := approvalapp.NewProjectionReconciler(")
	require.Contains(t, main, "go internalAppDispatcher.Run(ctx, 0)")
	require.Contains(t, main, "go approvalProjectionReconciler.Run(ctx, 0)")
	require.Contains(t, main, "invocation.NewScheduleDispatcher(")
	require.Contains(t, main, "invocation.NewScheduleWorker(")
	require.Contains(t, main, "workflowInvocationScheduleWorker.Start(ctx)")
	require.Less(
		t,
		strings.Index(main, "errCh <- server.Serve(listener)"),
		strings.Index(main, "workflowInvocationScheduleWorker.Start(ctx)"),
	)
	require.Less(
		t,
		strings.Index(main, "agentDeps.ApprovalSubjects = approvalSubjects"),
		strings.Index(main, "agentAPI := agent.New("),
	)
}

func TestE2EServerInjectsTeamStoreIntoAgentRuntime(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)

	main := string(source)
	agentDepsStart := strings.Index(main, "agentDeps := agent.StorePortsDeps{")
	require.NotEqual(t, -1, agentDepsStart)
	agentDepsEnd := strings.Index(main[agentDepsStart:], "\n\t}")
	require.NotEqual(t, -1, agentDepsEnd)
	agentDeps := main[agentDepsStart : agentDepsStart+agentDepsEnd]
	require.Contains(t, agentDeps, "Teams:     stores.Team,")
}

func TestE2EServerWiresFirstClassWorkflowRunRuntime(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)
	main := string(source)

	for _, expected := range []string{
		"workflowRunStore := run.New(corePool)",
		"workflowRunService := runmanage.New(workflowRunStore)",
		"orchestrator.NewCallbackService(workflowRunStore",
		"ScopeAuthorizer:  stores.App",
		"TaskWatchMutator: stores.App",
		"orchestrator.NewApprovalDecisionService(workflowRunStore)",
		"approvalHandler := approvalapp.NewHandler(",
		"approvalHandler.SetWorkflowRunDecisionSink(workflowApprovalDecisions)",
		"internalAppHandlers.Register(approvalapp.HandlerKey, approvalHandler)",
		"agentAPI.WorkflowPlans = workflowPlanService",
		"agentAPI.WorkflowPlanAgentEditGuard = workflowAgentEditService",
		"agentAPI.WorkflowRuns = workflowRunService",
		"agentAPI.WorkflowRunCallbacks = workflowRunCallbacks",
		"fanout.NewWorkflowRunMessageWriter",
		"WithReceiptResolver(fanout.NewWorkflowRunMessageReceiptStore(corePool))",
		"orchestrator.NewHumanApprovalAssigneeResolver(corePool)",
		"orchestrator.NewPinnedMaterialLoader(workflowPlanStore, workflowInvocationStore)",
		"orchestrator.NewEngine(workflowRunStore, workflowRunMaterials",
		"orchestrator.NewEffectDispatcher(",
		"approvalAppProjector,",
		"workflowRunApprovalAssignees,",
		"workflowRunWorker := orchestrator.NewWorker(",
		"WorkflowRun: runapi.Dependencies{",
		"Service:                   workflowRunService,",
		"Calendar:                  workflowCalendarService,",
		"workflowRunWorker.Start(ctx)",
	} {
		require.Contains(t, main, expected)
	}

	registryStart := strings.Index(main, "internalAppHandlers := appruntime.NewInternalHandlerRegistry()")
	approvalHandler := strings.Index(main, "internalAppHandlers.Register(approvalapp.HandlerKey, approvalHandler)")
	registrySeal := strings.Index(main, "internalAppHandlers.Seal()")
	require.NotEqual(t, -1, registryStart)
	require.NotEqual(t, -1, approvalHandler)
	require.NotEqual(t, -1, registrySeal)
	require.Less(t, registryStart, approvalHandler)
	require.Less(t, approvalHandler, registrySeal, "Approval App actions must be registered before the handler registry is sealed")

	require.Less(t, strings.Index(main, "agentAPI.WorkflowRunCallbacks = workflowRunCallbacks"), strings.Index(main, "routerDependencies := router.Dependencies{"))
	require.Less(t, strings.Index(main, "WorkflowRun: runapi.Dependencies{"), strings.Index(main, "routerDependencies.Validate()"))
	require.Less(t, strings.Index(main, "errCh <- server.Serve(listener)"), strings.Index(main, "workflowRunWorker.Start(ctx)"))
}

func TestE2EServerWiresTeamWorkflowAuthoringAndBrowserViews(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)

	main := string(source)
	for _, expected := range []string{
		"teamWorkflows := serviceteamworkflow.New(corePool)",
		"teamWorkflowBrowserService := teamworkflow.NewStoreService(corePool, teamWorkflows)",
		"agentAPI.TeamWorkflowPlans = teamWorkflows",
		"TeamWorkflow: teamworkflow.Dependencies{",
		"Service:                   teamWorkflowBrowserService,",
		"DirectPublishGuard:        teamWorkflows,",
	} {
		require.Contains(t, main, expected)
	}
	require.NotContains(t, main, "NewTeamWorkflowSubjectHandler")
}

func TestE2EServerWiresDaemonSessionIssuer(t *testing.T) {
	source, err := os.ReadFile("main.go")
	require.NoError(t, err)

	require.Contains(t, string(source), "SessionIssuer: identityStore,")
}

func TestE2EOrganizationCoreDependenciesSetDefaultWorkspaceGroup(t *testing.T) {
	deps := e2eOrganizationCoreDependencies(&store.Graph{})

	require.Equal(t, config.DefaultWorkspaceGroupID, deps.DefaultWorkspaceGroupID)
}

type fakeVisibleScopeQueryer struct{}

func (fakeVisibleScopeQueryer) Query(context.Context, string, ...any) (coredb.Rows, error) {
	panic("visible scope validation must not query storage")
}

func (fakeVisibleScopeQueryer) QueryRow(context.Context, string, ...any) coredb.Row {
	panic("visible scope validation must not query storage")
}

func TestStartTiDBUsesUnistoreRuntimeByDefault(t *testing.T) {
	t.Setenv("AGENTTEAM_E2E_TIDB_DSN", "")
	t.Setenv("AGENTTEAM_TIDB_TEST_DSN", "")
	t.Setenv("AGENTTEAM_E2E_TIDB_ADMIN_DSN", "")
	t.Setenv("AGENTTEAM_TIDB_TEST_ADMIN_DSN", "")

	originalStartUnistore := startUnistoreTiDBWithCleanupFn
	originalCreateDatabase := createTiDBDatabaseFn
	originalDropDatabase := dropTiDBDatabaseFn
	t.Cleanup(func() {
		startUnistoreTiDBWithCleanupFn = originalStartUnistore
		createTiDBDatabaseFn = originalCreateDatabase
		dropTiDBDatabaseFn = originalDropDatabase
	})

	var started bool
	var stopped bool
	var createdAdminDSN string
	var createdDatabaseName string
	var droppedAdminDSN string
	var droppedDatabaseName string

	startUnistoreTiDBWithCleanupFn = func(ctx context.Context) (string, func(context.Context) error, error) {
		started = true
		require.NotNil(t, ctx)
		return "root@tcp(127.0.0.1:4000)/?multiStatements=true&parseTime=true&loc=UTC", func(context.Context) error {
			stopped = true
			return nil
		}, nil
	}
	createTiDBDatabaseFn = func(ctx context.Context, adminDSN string, databaseName string) error {
		require.NotNil(t, ctx)
		createdAdminDSN = adminDSN
		createdDatabaseName = databaseName
		return nil
	}
	dropTiDBDatabaseFn = func(ctx context.Context, adminDSN string, databaseName string) error {
		require.NotNil(t, ctx)
		droppedAdminDSN = adminDSN
		droppedDatabaseName = databaseName
		return nil
	}

	var logs bytes.Buffer
	runtime, err := startTiDB(context.Background(), slog.New(slog.NewTextHandler(&logs, nil)))
	require.NoError(t, err)
	require.True(t, started)
	require.Equal(t, createdAdminDSN, "root@tcp(127.0.0.1:4000)/?multiStatements=true&parseTime=true&loc=UTC")
	require.NotEmpty(t, createdDatabaseName)
	require.Contains(t, runtime.DatabaseDSN, "/"+createdDatabaseName+"?")
	require.Contains(t, logs.String(), "starting local unistore tidb runtime")

	runtime.Close()
	require.Equal(t, createdAdminDSN, droppedAdminDSN)
	require.Equal(t, createdDatabaseName, droppedDatabaseName)
	require.True(t, stopped)
}
