package execution

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/coredb/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	executionstore "github.com/tidbcloud/AgentTeam/internal/store/role/execution"
)

type roleSubmissionFixture struct {
	WorkspaceID       string
	ManagerUserID     string
	ManagerMemberID   string
	OtherUserID       string
	ApplicantAgentID  string
	ApplicantMemberID string
	ExpectedMachineID string
	OtherMemberID     string
	SameManagerMachID string
	OtherManagerMach  string
	Principal         agent.AuthenticatedAgent
}

func seedRoleSubmissionFixture(
	t testing.TB,
	db coredb.QueryExecer,
	suffix string,
	now time.Time,
) roleSubmissionFixture {
	t.Helper()
	f := roleSubmissionFixture{
		WorkspaceID:       "wsp_rolesvc_" + suffix,
		ManagerUserID:     "usr_rolesvc_mgr_" + suffix,
		ManagerMemberID:   "mem_rolesvc_mgr_" + suffix,
		OtherUserID:       "usr_rolesvc_other_" + suffix,
		ApplicantAgentID:  "agt_rolesvc_" + suffix,
		ApplicantMemberID: "mem_rolesvc_agent_" + suffix,
		ExpectedMachineID: "mac_rolesvc_" + suffix,
		OtherMemberID:     "mem_rolesvc_other_" + suffix,
		SameManagerMachID: "mac_rolesvc_same_" + suffix,
		OtherManagerMach:  "mac_rolesvc_other_" + suffix,
	}
	orgID := "org_rolesvc_" + suffix
	mustExecRoleService(t, db, `
INSERT INTO users (id,email,name,display_name,password_hash,created_at,updated_at)
VALUES ($1,$2,$1,$1,'hash',$3,$3),($4,$5,$4,$4,'hash',$3,$3)`,
		f.ManagerUserID, f.ManagerUserID+"@example.com", now,
		f.OtherUserID, f.OtherUserID+"@example.com")
	mustExecRoleService(t, db, `
INSERT INTO organizations (id,slug,name,created_by_user_id,created_at,updated_at)
VALUES ($1,$1,$1,$2,$3,$3)`, orgID, f.ManagerUserID, now)
	mustExecRoleService(t, db, `
INSERT INTO workspaces (id,organization_id,slug,name,created_at,updated_at)
VALUES ($1,$2,$1,$1,$3,$3)`, f.WorkspaceID, orgID, now)
	mustExecRoleService(t, db, `
INSERT INTO members (id,workspace_id,type,display_name,user_id,workspace_role,status,created_at,updated_at)
VALUES
($1,$2,'human',$1,$3,'owner','active',$6,$6),
($4,$2,'agent',$4,NULL,NULL,'active',$6,$6),
($5,$2,'agent',$5,NULL,NULL,'active',$6,$6)`,
		f.ManagerMemberID, f.WorkspaceID, f.ManagerUserID,
		f.ApplicantMemberID, f.OtherMemberID, now)
	mustExecRoleService(t, db, `
INSERT INTO machines (id,workspace_id,created_by_user_id,name,api_key,status,created_at,updated_at)
VALUES
($1,$4,$5,$1,$1,'active',$7,$7),
($2,$4,$5,$2,$2,'active',$7,$7),
($3,$4,$6,$3,$3,'active',$7,$7)`,
		f.ExpectedMachineID, f.SameManagerMachID, f.OtherManagerMach,
		f.WorkspaceID, f.ManagerUserID, f.OtherUserID, now)
	mustExecRoleService(t, db, `
INSERT INTO agents (
  id,workspace_id,machine_id,member_id,name,runtime,model,description,
  auth_token_hash,status,activity_detail,created_at,updated_at
) VALUES ($1,$2,$3,$4,$1,'codex','gpt-5','','hash','inactive','',$5,$5)`,
		f.ApplicantAgentID, f.WorkspaceID, f.ExpectedMachineID,
		f.ApplicantMemberID, now)
	f.Principal = agent.AuthenticatedAgent{
		WorkspaceID: f.WorkspaceID, AgentID: f.ApplicantAgentID,
		MemberID: f.ApplicantMemberID, MachineID: f.ExpectedMachineID,
	}
	return f
}

func newRoleSubmissionService(
	db coredb.DB,
	preflight func(context.Context, PreflightCommand) error,
) *Service {
	approvals := approval.New(db)
	core := CoreSubmissionStore{DB: db, Approvals: approvals}
	return NewService(Dependencies{
		AuthenticateApplicant: core.AuthenticateApplicant,
		LookupApproval:        core.LookupApproval,
		ListAssignments:       core.ListAssignments,
		ResolveManager:        core.ResolveManager,
		Preflight:             preflight,
		CommitSubmission:      core.CommitSubmission,
		LoadExecution: func(context.Context, string, string) (executionstore.Execution, error) {
			return executionstore.Execution{}, executionstore.ErrNotFound
		},
	})
}

func startRoleSubmissionConcurrencyDB(t *testing.T) coredb.DB {
	t.Helper()
	ctx := context.Background()
	dsn := testkit.StartInitializedTiDBWithRootDSN(t, roleServiceTiDB.RootDSN(t))
	sqlDB, err := testkit.OpenTiDBContext(ctx, dsn)
	require.NoError(t, err)
	sqlDB.SetMaxOpenConns(8)
	sqlDB.SetMaxIdleConns(8)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
	db := coredb.WrapSQLDB(sqlDB)
	var version string
	require.NoError(t, db.QueryRow(ctx, "SELECT VERSION()").Scan(&version))
	require.Contains(t, version, "TiDB")
	t.Logf("real TiDB role submission concurrency gate: %s", version)
	return db
}

func TestRoleSubmissionCreatesApprovalAndReplaysBeforeMutableFacts(t *testing.T) {
	ctx := context.Background()
	db := roleServiceTiDB.SerializedRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	f := seedRoleSubmissionFixture(t, db, "flow", now)
	preflights := 0
	service := newRoleSubmissionService(db, func(_ context.Context, command PreflightCommand) error {
		preflights++
		require.Equal(t, f.Principal, command.Principal)
		require.Equal(t, f.ManagerUserID, command.Manager.UserID)
		return nil
	})
	command := SubmitCommand{
		Principal: f.Principal, RequestID: "req_1",
		Proposal: RoleCreateProposal{Name: "Release", Definition: "Review"},
		Now:      now,
	}
	first, err := service.Submit(ctx, command)
	require.NoError(t, err)
	require.Equal(t, RequestPending, first.Status)
	require.True(t, first.Created)
	require.Equal(t, 1, preflights)
	assignments, err := approval.New(db).ListAssignments(ctx, f.WorkspaceID, first.ApprovalID)
	require.NoError(t, err)
	require.Len(t, assignments, 1)
	require.Equal(t, f.ManagerMemberID, assignments[0].ApproverMemberID)

	// Drift every mutable fact after the request was durably accepted.
	mustExecRoleService(t, db, `UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`,
		f.OtherManagerMach, f.WorkspaceID, f.ApplicantAgentID)
	mustExecRoleService(t, db, `UPDATE members SET status='inactive' WHERE workspace_id=$1 AND id=$2`,
		f.WorkspaceID, f.ManagerMemberID)
	replayed, err := service.Submit(ctx, command)
	require.NoError(t, err)
	require.Equal(t, first.ApprovalID, replayed.ApprovalID)
	require.False(t, replayed.Created)
	require.Equal(t, 1, preflights, "outer exact replay must not run mutable preflight")
}

func TestRoleSubmissionFinalFenceRejectsDriftWithoutApproval(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Microsecond)
	tests := []struct {
		name   string
		mutate func(testing.TB, coredb.QueryExecer, roleSubmissionFixture)
	}{
		{"manager inactive", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE members SET status='inactive' WHERE workspace_id=$1 AND id=$2`, f.WorkspaceID, f.ManagerMemberID)
		}},
		{"machine inactive", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE machines SET status='inactive' WHERE workspace_id=$1 AND id=$2`, f.WorkspaceID, f.ExpectedMachineID)
		}},
		{"machine creator drift", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE machines SET created_by_user_id=$1 WHERE workspace_id=$2 AND id=$3`, f.OtherUserID, f.WorkspaceID, f.ExpectedMachineID)
		}},
		{"Agent archived", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE agents SET status='archived' WHERE workspace_id=$1 AND id=$2`, f.WorkspaceID, f.ApplicantAgentID)
		}},
		{"Agent member rebind", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE agents SET member_id=$1 WHERE workspace_id=$2 AND id=$3`, f.OtherMemberID, f.WorkspaceID, f.ApplicantAgentID)
		}},
		{"Agent same-manager machine rebind", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`, f.SameManagerMachID, f.WorkspaceID, f.ApplicantAgentID)
		}},
		{"Agent other-manager machine rebind", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`, f.OtherManagerMach, f.WorkspaceID, f.ApplicantAgentID)
		}},
		{"Agent member inactive", func(t testing.TB, db coredb.QueryExecer, f roleSubmissionFixture) {
			mustExecRoleService(t, db, `UPDATE members SET status='inactive' WHERE workspace_id=$1 AND id=$2`, f.WorkspaceID, f.ApplicantMemberID)
		}},
	}
	for index, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			db := roleServiceTiDB.SerializedRollbackDB(t, ctx)
			f := seedRoleSubmissionFixture(t, db, fmt.Sprintf("drift_%d", index), now)
			service := newRoleSubmissionService(db, func(context.Context, PreflightCommand) error {
				test.mutate(t, db, f)
				return nil
			})
			_, err := service.Submit(ctx, SubmitCommand{
				Principal: f.Principal, RequestID: "req_drift",
				Proposal: RoleCreateProposal{Name: "Release", Definition: "Review"},
				Now:      now,
			})
			require.Error(t, err)
			var count int
			require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*) FROM approval_instances
WHERE workspace_id=$1 AND subject_type='role_management'`,
				f.WorkspaceID,
			).Scan(&count))
			require.Zero(t, count)
		})
	}
}

func TestRoleSubmissionInnerReplayWinsAfterConcurrentMiss(t *testing.T) {
	ctx := context.Background()
	db := roleServiceTiDB.SerializedRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	f := seedRoleSubmissionFixture(t, db, "inner_replay", now)
	core := CoreSubmissionStore{DB: db, Approvals: approval.New(db)}
	first := newRoleSubmissionService(db, func(context.Context, PreflightCommand) error { return nil })
	command := SubmitCommand{
		Principal: f.Principal, RequestID: "req_inner",
		Proposal: RoleCreateProposal{Name: "Release", Definition: "Review"},
		Now:      now,
	}
	// Freeze the prepared commit while outer lookup still reports a miss.
	var injected bool
	service := NewService(Dependencies{
		AuthenticateApplicant: core.AuthenticateApplicant,
		LookupApproval: func(ctx context.Context, lookup ApprovalLookup) (approval.Instance, bool, error) {
			if !injected {
				injected = true
				created, err := first.Submit(ctx, command)
				require.NoError(t, err)
				require.NotEmpty(t, created.ApprovalID)
				return approval.Instance{}, false, nil
			}
			return core.LookupApproval(ctx, lookup)
		},
		ListAssignments: core.ListAssignments,
		ResolveManager:  core.ResolveManager,
		Preflight: func(context.Context, PreflightCommand) error {
			// Drift after the winner committed and after this submission's
			// initial authentication/manager snapshot. Inner replay must return
			// before final runtime/manager locks or these facts would fail.
			mustExecRoleService(t, db, `UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`,
				f.OtherManagerMach, f.WorkspaceID, f.ApplicantAgentID)
			mustExecRoleService(t, db, `UPDATE members SET status='inactive' WHERE workspace_id=$1 AND id=$2`,
				f.WorkspaceID, f.ManagerMemberID)
			return nil
		},
		CommitSubmission: core.CommitSubmission,
		LoadExecution: func(context.Context, string, string) (executionstore.Execution, error) {
			return executionstore.Execution{}, executionstore.ErrNotFound
		},
	})
	result, err := service.Submit(ctx, command)
	require.NoError(t, err)
	require.False(t, result.Created)
	require.NotEmpty(t, result.ApprovalID)
}

func TestRoleSubmissionInnerReplayRejectsAssignmentManagerMismatch(t *testing.T) {
	ctx := context.Background()
	db := roleServiceTiDB.SerializedRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	f := seedRoleSubmissionFixture(t, db, "inner_manager_mismatch", now)
	core := CoreSubmissionStore{DB: db, Approvals: approval.New(db)}
	service := newRoleSubmissionService(db, func(context.Context, PreflightCommand) error { return nil })
	command := SubmitCommand{
		Principal: f.Principal, RequestID: "req_inner_manager",
		Proposal: RoleCreateProposal{Name: "Release", Definition: "Review"},
		Now:      now,
	}
	created, err := service.Submit(ctx, command)
	require.NoError(t, err)

	mustExecRoleService(t, db, `
UPDATE approval_assignments
SET approver_member_id=$1
WHERE workspace_id=$2 AND approval_instance_id=$3`,
		f.OtherMemberID, f.WorkspaceID, created.ApprovalID,
	)
	prepared, err := prepareSubmission(command)
	require.NoError(t, err)
	_, _, err = core.CommitSubmission(ctx, SubmissionCommit{
		Principal:         prepared.command.Principal,
		Manager:           Manager{UserID: f.ManagerUserID, MemberID: f.ManagerMemberID},
		Payload:           prepared.payload,
		DeterministicJSON: prepared.deterministicJSON,
		PayloadDigest:     prepared.payloadDigest,
		IdempotencyKey:    prepared.idempotencyKey,
		Now:               command.Now,
	})
	require.ErrorIs(t, err, ErrConflict)
}

func TestRoleSubmissionLocksSerializeMachineRebind(t *testing.T) {
	ctx := context.Background()
	db := startRoleSubmissionConcurrencyDB(t)
	now := time.Now().UTC().Truncate(time.Microsecond)
	f := seedRoleSubmissionFixture(t, db, "lock_rebind", now)

	finalTx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	t.Cleanup(func() { _ = finalTx.Rollback(context.Background()) })
	require.NoError(t, access.LockAndRevalidateAgentPrincipalManager(
		ctx,
		finalTx,
		access.AuthenticatedAgentPrincipal{
			WorkspaceID: f.Principal.WorkspaceID, AgentID: f.Principal.AgentID,
			MemberID: f.Principal.MemberID, MachineID: f.Principal.MachineID,
		},
		access.HumanManager{UserID: f.ManagerUserID, MemberID: f.ManagerMemberID},
	))

	rebindStarted := make(chan struct{})
	rebindDone := make(chan error, 1)
	var once sync.Once
	go func() {
		once.Do(func() { close(rebindStarted) })
		_, updateErr := db.Exec(ctx, `
UPDATE agents SET machine_id=$1
WHERE workspace_id=$2 AND id=$3`,
			f.OtherManagerMach, f.WorkspaceID, f.ApplicantAgentID)
		rebindDone <- updateErr
	}()
	<-rebindStarted
	select {
	case err := <-rebindDone:
		t.Fatalf("machine rebind bypassed final submission locks: %v", err)
	case <-time.After(300 * time.Millisecond):
		// Expected: the Agent row remains locked by the final submission tx.
	}
	require.NoError(t, finalTx.Commit(ctx))
	select {
	case err := <-rebindDone:
		require.NoError(t, err)
	case <-time.After(10 * time.Second):
		t.Fatal("machine rebind did not resume after final submission commit")
	}

	verifyTx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	defer verifyTx.Rollback(ctx)
	err = access.LockAndRevalidateAgentPrincipalManager(
		ctx,
		verifyTx,
		access.AuthenticatedAgentPrincipal{
			WorkspaceID: f.Principal.WorkspaceID, AgentID: f.Principal.AgentID,
			MemberID: f.Principal.MemberID, MachineID: f.Principal.MachineID,
		},
		access.HumanManager{UserID: f.ManagerUserID, MemberID: f.ManagerMemberID},
	)
	require.Error(t, err, "old authenticated principal must fail after rebind commits")
}
