package server

import (
	"context"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/router"
	"github.com/tidbcloud/AgentTeam/internal/workflow/invocation"
)

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

	require.Equal(t, "workspace-a", schedulerWorkspaceGroupIDForConfig(config.ServerConfig{Profile: config.ServerProfileWorkspace, WorkspaceGroupID: " workspace-a "}))
	require.Empty(t, schedulerWorkspaceGroupIDForConfig(config.ServerConfig{Profile: config.ServerProfileAll, WorkspaceGroupID: "workspace-a"}))
	require.Empty(t, schedulerWorkspaceGroupIDForConfig(config.ServerConfig{Profile: config.ServerProfileCentral, WorkspaceGroupID: "workspace-a"}))
}

func TestEffectiveMeshXEndpointUsesEmbeddedTransport(t *testing.T) {
	embeddedHits := 0
	embedded := roundTripFunc(func(request *http.Request) (*http.Response, error) {
		embeddedHits++
		return &http.Response{StatusCode: http.StatusNoContent, Header: make(http.Header), Body: http.NoBody, Request: request}, nil
	})
	baseURL, client := effectiveMeshXEndpoint(config.Config{}, nil, router.ProfileWorkspace, embedded)
	require.Equal(t, "http://embedded.meshx", baseURL)
	request, err := http.NewRequest(http.MethodGet, baseURL+"/readyz", nil)
	require.NoError(t, err)
	response, err := client.Do(request)
	require.NoError(t, err)
	require.NoError(t, response.Body.Close())
	require.Equal(t, 1, embeddedHits)

	baseURL, client = effectiveMeshXEndpoint(config.Config{}, nil, router.ProfileWorkspace, nil)
	require.Empty(t, baseURL)
	require.Nil(t, client)

	baseURL, client = effectiveMeshXEndpoint(config.Config{}, nil, router.ProfileCentral, nil)
	require.Empty(t, baseURL)
	require.Nil(t, client)
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return fn(request) }

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

	require.Nil(t, meshXAssetAccessWorkspaceIDs(config.ServerConfig{Profile: config.ServerProfileAll}, nil))

	lister := &fakeMeshXAssetWorkspaceLister{workspaceIDs: []string{"wsp_1", "wsp_2"}}
	source := meshXAssetAccessWorkspaceIDs(config.ServerConfig{
		Profile:          config.ServerProfileWorkspace,
		WorkspaceGroupID: " workspace-a ",
	}, lister)
	require.NotNil(t, source)

	ids, err := source(context.Background())
	require.NoError(t, err)
	require.Equal(t, []string{"wsp_1", "wsp_2"}, ids)
	require.Equal(t, "workspace-a", lister.groupID)
}

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

	worker, err := newWorkflowRunWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileCentral},
	}, nil, nil, nil, nil, nil, nil, nil, nil)

	require.NoError(t, err)
	require.Nil(t, worker)
}

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

	_, err := newWorkflowRunWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileWorkspace},
	}, nil, nil, nil, nil, nil, nil, nil, nil)

	require.EqualError(t, err, "AGENTTEAM_WORKSPACE_GROUP_ID is required when AGENTTEAM_SERVER_PROFILE=workspace")
}

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

	_, err := newWorkflowRunWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileAll},
	}, nil, nil, nil, nil, nil, nil, nil, nil)

	require.EqualError(t, err, "build Workflow Run worker: Run store is required")
}

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

	worker, err := newWorkflowInvocationScheduleWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileCentral},
	}, nil, nil)
	require.NoError(t, err)
	require.Nil(t, worker)

	_, err = newWorkflowInvocationScheduleWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileWorkspace},
	}, nil, fakeWorkflowInvocationScheduleMaterializer{})
	require.EqualError(t, err, "AGENTTEAM_WORKSPACE_GROUP_ID is required when AGENTTEAM_SERVER_PROFILE=workspace")

	worker, err = newWorkflowInvocationScheduleWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileWorkspace, WorkspaceGroupID: "workspace-a"},
	}, nil, fakeWorkflowInvocationScheduleMaterializer{})
	require.NoError(t, err)
	require.NotNil(t, worker)

	worker, err = newWorkflowInvocationScheduleWorkerForConfig(config.Config{
		Server: config.ServerConfig{Profile: config.ServerProfileAll},
	}, nil, fakeWorkflowInvocationScheduleMaterializer{})
	require.NoError(t, err)
	require.NotNil(t, worker)
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	require.Contains(t, wire, "invocation.NewScheduleDispatcher")
	require.Contains(t, wire, "invocation.NewScheduleWorker")
	require.Regexp(t, `workflowInvocationSchedules:\s+workflowInvocationScheduleWorker,`, wire)
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	registryStart := strings.Index(wire, "internalAppHandlers := appruntime.NewInternalHandlerRegistry()")
	require.NotEqual(t, -1, registryStart)
	registryEnd := strings.Index(wire[registryStart:], "internalAppDispatcher := appruntime.NewInternalDispatcher")
	require.NotEqual(t, -1, registryEnd)
	registryWiring := wire[registryStart : registryStart+registryEnd]
	require.Contains(t, wire, "teamRoleProposals := roleproposal.New(inf.coreDataDB)")
	require.Contains(t, registryWiring, "approvalSubjects := approvalapp.NewSubjectRegistry(stores.Approval)")
	require.Contains(t, registryWiring, "approvalapp.NewTeamRoleProposalSubjectHandler(")
	require.Contains(t, registryWiring, "approvalapp.NewAgentManagementSubjectHandler(")
	require.Contains(t, registryWiring, "approvalapp.NewSkillDeleteSubjectHandler(")
	require.Contains(t, wire, "teamWorkflows := teamworkflow.New(inf.coreDataDB")
	require.Contains(t, wire, "LifecycleEvents: liveEmitter")
	require.Contains(t, wire, "agentManagement := agentapproval.New(inf.coreDataDB, stores.Approval, stores.AgentOps)")
	require.Contains(t, registryWiring, "agentDeps.ApprovalSubjects = approvalSubjects")
	require.Contains(t, registryWiring, "internalAppHandlers.Register(approvalapp.HandlerKey")
	require.Contains(
		t,
		registryWiring,
		"stores.Approval, stores.App, approvalAppProjector, approvalSubjects,",
	)
	require.Contains(t, registryWiring, "approvalAppProjector.Register()")
	require.Contains(t, registryWiring, "internalAppHandlers.Seal()")
	require.Regexp(t, `internalAppDispatcher:\s+internalAppDispatcher,`, wire)
	require.Regexp(t, `approvalProjectionReconciler:\s+approvalProjectionReconciler,`, wire)
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)
	roleSource, err := os.ReadFile("role_management.go")
	require.NoError(t, err)

	wire := string(source)
	role := string(roleSource)
	combined := wire + role
	normalized := strings.Join(strings.Fields(wire), " ")
	require.Equal(t, 1, strings.Count(wire, "newRoleManagementRuntimeForConfig("))
	require.Equal(t, 1, strings.Count(
		combined, "forwarder.NewRoleOperationGateway(",
	))
	require.Equal(t, 1, strings.Count(
		combined, "execution.NewService(",
	))
	require.Equal(t, 1, strings.Count(
		combined, "executionstore.New(",
	))
	require.Equal(t, 1, strings.Count(
		wire, "approvalapp.NewRoleManagementSubjectHandler(",
	))
	require.Equal(t, 1, strings.Count(
		wire, "approvalapp.NewRoleManagementTerminalObserver(",
	))
	require.Equal(t, 1, strings.Count(
		normalized, "ProjectTerminal: roleManagementTerminalObserver.ProjectTerminal,",
	))
	require.Equal(t, 1, strings.Count(
		normalized, "LoadProjectedTerminal: roleManagementTerminalObserver.LoadProjectedTerminal,",
	))
	gatewayGuard := `if ownsMeshXRuntime && strings.TrimSpace(apiForwarderDeps.BaseURL) != "" &&
		cfg.Server.ProfileOrDefault() != config.ServerProfileCentral {
		roleOperationGateway, err = forwarder.NewRoleOperationGateway(`
	require.Equal(t, 1, strings.Count(wire, gatewayGuard))
	require.Equal(t, 1, strings.Count(wire, "BaseURL:                    meshXBaseURL,"))
	require.Equal(t, 1, strings.Count(wire, "HTTPClient:                 meshXHTTPClient,"))
	require.Less(
		t, strings.Index(wire, "approvalAppProjector.Register()"),
		strings.Index(wire, "approvalapp.NewRoleManagementTerminalObserver("),
	)
	require.Less(
		t,
		strings.Index(wire, "approvalapp.NewRoleManagementTerminalObserver("),
		strings.Index(wire, "newRoleManagementRuntimeForConfig("),
	)
	require.Equal(t, 1, strings.Count(
		strings.Join(strings.Fields(wire), " "),
		"roleManagementWorker *execution.Worker",
	))
	require.Equal(t, 1, strings.Count(
		wire,
		"roleManagementWorker: roleManagementWorker,",
	))
	require.Contains(t, wire, "roleManagementService := roleManagementRuntime.service")
	require.Contains(t, wire, "roleManagementWorker := roleManagementRuntime.worker")
	require.Contains(t, wire, "if roleManagementRuntime != nil {")
	require.Less(
		t,
		strings.Index(wire, "newRoleManagementRuntimeForConfig("),
		strings.Index(wire, "if roleManagementRuntime != nil {"),
	)
	require.Less(
		t,
		strings.Index(wire, "if roleManagementRuntime != nil {"),
		strings.Index(wire, "approvalapp.NewRoleManagementSubjectHandler("),
	)
	require.Less(
		t,
		strings.Index(wire, "approvalapp.NewRoleManagementSubjectHandler("),
		strings.Index(wire, "approvalSubjects.Seal()"),
	)
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)
	wire := string(source)
	normalized := strings.Join(strings.Fields(wire), " ")

	require.Equal(t, 1, strings.Count(wire, "forwarder.NewRoleReadGateway("))
	gatewayGuard := `if ownsMeshXRuntime && strings.TrimSpace(apiForwarderDeps.BaseURL) != "" &&
		cfg.Server.ProfileOrDefault() != config.ServerProfileCentral {`
	guardStart := strings.Index(wire, gatewayGuard)
	require.NotEqual(t, -1, guardStart)
	guardEnd := strings.Index(wire[guardStart:], "\n\t}")
	require.NotEqual(t, -1, guardEnd)
	require.Contains(t, wire[guardStart:guardStart+guardEnd], "forwarder.NewRoleReadGateway(")

	agentNew := strings.Index(wire, "agentAPI := agent.New(")
	routerAgent := strings.Index(normalized, "Agent: agentAPI,")
	require.NotEqual(t, -1, agentNew)
	require.NotEqual(t, -1, routerAgent)
	normalizedAgentNew := strings.Index(normalized, "agentAPI := agent.New(")
	require.Less(t, normalizedAgentNew, routerAgent)
	composition := normalized[normalizedAgentNew:routerAgent]
	require.Equal(t, 1, strings.Count(composition,
		"if roleManagementRuntime != nil && roleReadGateway != nil {"))
	require.Equal(t, 1, strings.Count(composition,
		"agentAPI.RoleManagers = roleManagementRuntime.manager"))
	require.Equal(t, 1, strings.Count(composition,
		"agentAPI.RoleReads = roleReadGateway"))
	require.Equal(t, 1, strings.Count(composition,
		"agentAPI.RoleRequests = roleManagementRuntime.service"))
	require.Contains(t, normalized,
		"agentAPI.RoleManagers = roleManagementRuntime.manager agentAPI.RoleReads = roleReadGateway agentAPI.RoleRequests = roleManagementRuntime.service")
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)
	wire := string(source)
	normalized := strings.Join(strings.Fields(wire), " ")

	require.Equal(t, 1, strings.Count(wire, "forwarder.NewSkillGateway("))
	gatewayGuard := `if ownsMeshXRuntime && strings.TrimSpace(apiForwarderDeps.BaseURL) != "" &&
		cfg.Server.ProfileOrDefault() != config.ServerProfileCentral {`
	guardStart := strings.Index(wire, gatewayGuard)
	require.NotEqual(t, -1, guardStart)
	guardEnd := strings.Index(wire[guardStart:], "\n\t}")
	require.NotEqual(t, -1, guardEnd)
	require.Contains(t, wire[guardStart:guardStart+guardEnd], "forwarder.NewSkillGateway(")

	require.Contains(t, normalized,
		"if skillGateway != nil { agentAPI.SkillOwners = stores.App agentAPI.SkillWorkspace = workspaceResolver agentAPI.Skills = skillGateway }")
	require.Equal(t, 1, strings.Count(wire, "agent.NewAgentSkillDeleteAuthority("))
	require.Equal(t, 1, strings.Count(wire, "approvalapp.NewSkillDeleteSubjectHandler("))
	require.Contains(t, normalized,
		"skillDeleteAuthority := agent.NewAgentSkillDeleteAuthority( stores.App, workspaceResolver, skillGateway, )")
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	agentDepsStart := strings.Index(wire, "agentDeps := agent.StorePortsDeps{")
	require.NotEqual(t, -1, agentDepsStart)
	agentDepsEnd := strings.Index(wire[agentDepsStart:], "\n\t}")
	require.NotEqual(t, -1, agentDepsEnd)
	agentDeps := wire[agentDepsStart : agentDepsStart+agentDepsEnd]
	// Whitespace-insensitive: gofmt re-aligns struct-literal fields when a new
	// field (e.g. ChannelManagement) shifts the column, so do not pin the spacing.
	require.Regexp(t, `Teams:\s+stores\.Team,`, agentDeps)
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	require.Contains(t, wire, "runstore.NewWithOptions")
	require.Contains(t, wire, "agentAPI.WorkflowPlans = workflowPlanService")
	require.Contains(t, wire, "agentAPI.WorkflowPlanAgentEditGuard = workflowAgentEditService")
	require.Contains(t, wire, "agentAPI.WorkflowRuns = workflowRunService")
	require.Contains(t, wire, "orchestrator.NewPinnedMaterialLoader")
	require.Contains(t, wire, "orchestrator.NewEngine")
	require.Contains(t, wire, "orchestrator.NewEffectDispatcher")
	require.Contains(t, wire, "orchestrator.NewWorker")
	require.Contains(t, wire, "fanout.NewWorkflowRunMessageWriter")
	require.Contains(t, wire, ".WithReceiptResolver(fanout.NewWorkflowRunMessageReceiptStore(inf.coreDataDB))")
	require.Contains(t, wire, "stores.App.SetTaskChangeSink(taskChangeSink)")
	require.Contains(t, wire, "TaskChangeSink: taskChangeSink")
	require.Contains(t, wire, "orchestrator.NewHumanApprovalAssigneeResolver(inf.coreDataDB)")
	require.Contains(t, wire, "orchestrator.NewCallbackService(workflowRunStore")
	require.Regexp(t, `ScopeAuthorizer:\s+stores\.App,`, wire)
	require.Contains(t, wire, "agentAPI.WorkflowRunCallbacks = workflowRunCallbacks")
	require.Contains(t, wire, "WorkflowRun: run.Dependencies{")
	require.Contains(t, wire, "workflowCalendarService := calendar.New(calendarstore.New(inf.coreDataDB))")
	require.Regexp(t, `Calendar:\s+workflowCalendarService,`, wire)
	require.Regexp(t, `workflowRunWorker:\s+workflowRunWorker,`, wire)
}

// TestProductionWireConstructsAndRunsWorkflowRunRecovery proves P0 #1: the
// formal production server constructs the automatic recovery runner exactly
// once and injects it into the first-class Workflow Run worker (not only
// agentteam-e2e-server). The runner is not started as its own goroutine — it runs
// inside the worker's tick via WorkerOptions.Recovery, so the production
// lifecycle owns exactly one runner with no duplicate, no leaked goroutine,
// and no shutdown race.
func TestProductionWireConstructsAndRunsWorkflowRunRecovery(t *testing.T) {
	t.Parallel()

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	require.Equal(t, 1, strings.Count(wire, "orchestrator.NewRecoveryRunner("))
	require.Contains(t, wire, "workflowRunRecovery, err := orchestrator.NewRecoveryRunner(")
	require.Contains(t, wire, `"workflow-run-recovery:" + instanceID`)
	require.Contains(t, wire, "WorkspaceGroupID: schedulerWorkspaceGroupIDForConfig(cfg.Server)")
	require.Contains(t, wire, "Recovery: workflowRunRecovery,")
	// The same production store and message writer feed the recovery runner,
	// and the same worker pass still reconciles leases (Reconciler unchanged).
	require.Contains(t, wire, "orchestrator.NewWorker(reducer, effects, orchestrator.WorkerOptions{")
	require.Contains(t, wire, "Reconciler: effects, Recovery: workflowRunRecovery,")
	require.NotContains(t, wire, "workflowRunRecovery.Start(")
	require.NotContains(t, wire, "go workflowRunRecovery")
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	for _, expected := range []string{
		"teamWorkflows := teamworkflow.New(inf.coreDataDB",
		"LifecycleEvents: liveEmitter",
		"teamWorkflowBrowserService := teamworkflowapi.NewStoreService(inf.coreDataDB, teamWorkflows)",
		"agentAPI.TeamWorkflowPlans = teamWorkflows",
		"TeamWorkflow: teamworkflowapi.Dependencies{",
		"Service:                   teamWorkflowBrowserService,",
		"DirectPublishGuard:        teamWorkflows,",
	} {
		require.Contains(t, wire, expected)
	}
	require.NotContains(t, wire, "NewTeamWorkflowSubjectHandler")
}

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

	source, err := os.ReadFile("wire.go")
	require.NoError(t, err)

	wire := string(source)
	registryStart := strings.Index(wire, "internalAppHandlers := appruntime.NewInternalHandlerRegistry()")
	require.NotEqual(t, -1, registryStart)
	registryEnd := strings.Index(wire[registryStart:], "internalAppHandlers.Seal()")
	require.NotEqual(t, -1, registryEnd)
	registryWiring := wire[registryStart : registryStart+registryEnd]
	require.Contains(t, registryWiring, "orchestrator.NewApprovalDecisionService(workflowRunStore)")
	require.Contains(t, registryWiring, "approvalHandler := approvalapp.NewHandler(")
	require.Contains(t, registryWiring, "approvalHandler.SetWorkflowRunDecisionSink(workflowApprovalDecisions)")
	require.Contains(t, registryWiring, "internalAppHandlers.Register(approvalapp.HandlerKey, approvalHandler)")
	require.NotContains(t, registryWiring, "workflowapp")
}

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

	provider := latestDaemonVersionProvider(config.DaemonConfig{
		LatestVersionOverride: "v9.9.9",
	}, nil)

	latest, err := provider.Latest(context.Background())
	require.NoError(t, err)
	require.Equal(t, "9.9.9", latest)
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{
			"schema_version": "1",
			"product": "agentteam-daemon",
			"channel": "stable",
			"current_version": "v1.0.2-rc.15"
		}`))
	}))
	t.Cleanup(server.Close)

	provider := latestDaemonVersionProvider(config.DaemonConfig{
		LatestVersionURL:      server.URL,
		LatestVersionCacheTTL: time.Hour,
	}, nil)

	latest, err := provider.Latest(context.Background())
	require.NoError(t, err)
	require.Equal(t, "1.0.2-rc.15", latest)
}

type fakeMeshXAssetWorkspaceLister struct {
	groupID      string
	workspaceIDs []string
}

type fakeWorkflowInvocationScheduleMaterializer struct{}

func (fakeWorkflowInvocationScheduleMaterializer) ListDueScheduleCandidates(context.Context, invocation.ListDueScheduleInput) ([]invocation.ScheduleCandidate, error) {
	return nil, nil
}

func (fakeWorkflowInvocationScheduleMaterializer) MaterializeScheduled(context.Context, invocation.ScheduleCandidate, time.Time) (invocation.ScheduleMaterializeResult, error) {
	return invocation.ScheduleMaterializeResult{}, nil
}

func (l *fakeMeshXAssetWorkspaceLister) ListRoutableWorkspaceIDsForGroup(_ context.Context, groupID string) ([]string, error) {
	l.groupID = groupID
	return append([]string(nil), l.workspaceIDs...), nil
}
