package execution

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
)

const executionAuthorityQueryPrefix = `SELECT applicant_machine\.created_by_user_id`

func TestCoreSubmissionStoreUsesOneWorkspaceFencedAuthorityQuery(t *testing.T) {
	store, mock := testExecutionAuthorityStore(t)
	command := testExecutionAuthorityCommand(testExecutionAuthorityPayload(ActionApply))
	mock.ExpectQuery(executionAuthorityQueryPrefix).
		WithArgs(
			command.Approval.WorkspaceID,
			command.Payload.ApplicantAgentID,
			command.Approval.ApplicantMemberID,
			command.RequiredManagerMemberID,
			"agent_target",
		).
		WillReturnRows(sqlmock.NewRows([]string{"created_by_user_id"}).
			AddRow("user_manager"))

	authority, err := store.RecheckExecutionAuthority(context.Background(), command)

	require.NoError(t, err)
	require.Equal(t, "user_manager", authority.ManagerUserID)
	require.Equal(t, []string{"agent_target"}, authority.OwnedAgentIDs,
		"apply may project only its approved target Agent")
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestCoreSubmissionStoreKeepsAuthorityLookupFailuresUncertain(t *testing.T) {
	for _, testCase := range []struct {
		name     string
		queryErr error
	}{
		{name: "database failure", queryErr: errors.New("database unavailable")},
		{name: "context canceled", queryErr: context.Canceled},
		{name: "context deadline exceeded", queryErr: context.DeadlineExceeded},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			store, mock := testExecutionAuthorityStore(t)
			command := testExecutionAuthorityCommand(testExecutionAuthorityPayload(ActionCreate))
			mock.ExpectQuery(executionAuthorityQueryPrefix).
				WithArgs(
					command.Approval.WorkspaceID,
					command.Payload.ApplicantAgentID,
					command.Approval.ApplicantMemberID,
					command.RequiredManagerMemberID,
					"",
				).
				WillReturnError(testCase.queryErr)

			_, err := store.RecheckExecutionAuthority(context.Background(), command)

			require.Error(t, err)
			require.NotErrorIs(t, err, ErrNotImplemented)
			var applyErr *ApplyError
			require.False(t, errors.As(err, &applyErr),
				"unknown lookup failures must remain retryable")
			require.ErrorIs(t, err, testCase.queryErr)
			require.NoError(t, mock.ExpectationsWereMet())
		})
	}
}

func TestCoreSubmissionStoreKeepsAuthorityScanFailureUncertain(t *testing.T) {
	store, mock := testExecutionAuthorityStore(t)
	command := testExecutionAuthorityCommand(testExecutionAuthorityPayload(ActionCreate))
	mock.ExpectQuery(executionAuthorityQueryPrefix).
		WithArgs(
			command.Approval.WorkspaceID,
			command.Payload.ApplicantAgentID,
			command.Approval.ApplicantMemberID,
			command.RequiredManagerMemberID,
			"",
		).
		WillReturnRows(sqlmock.NewRows([]string{"created_by_user_id"}).AddRow(nil))

	_, err := store.RecheckExecutionAuthority(context.Background(), command)

	require.Error(t, err)
	require.NotErrorIs(t, err, ErrNotImplemented)
	var applyErr *ApplyError
	require.False(t, errors.As(err, &applyErr),
		"scan failures must remain retryable")
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestCoreSubmissionStoreRejectsMalformedAuthorityCommandsAsUncertain(t *testing.T) {
	valid := testExecutionAuthorityCommand(testExecutionAuthorityPayload(ActionCreate))
	for _, testCase := range []struct {
		name   string
		mutate func(*ExecutionAuthorityCommand)
	}{
		{
			name: "blank workspace",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.Approval.WorkspaceID = ""
			},
		},
		{
			name: "blank durable applicant Agent",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.Payload.ApplicantAgentID = ""
			},
		},
		{
			name: "blank Approval applicant member",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.Approval.ApplicantMemberID = ""
			},
		},
		{
			name: "blank required decider",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.RequiredManagerMemberID = ""
			},
		},
		{
			name: "blank Approval decider",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.Approval.DecidedByMemberID = ""
			},
		},
		{
			name: "required decider differs from Approval decider",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.RequiredManagerMemberID = "mem_other_manager"
			},
		},
		{
			name: "Approval and payload action mismatch",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.Payload.Action = ActionApply
			},
		},
		{
			name: "apply payload missing target command",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.Payload = testExecutionAuthorityPayload(ActionApply)
				command.ApplyTargetAgentID = ""
			},
		},
		{
			name: "non-apply command includes target",
			mutate: func(command *ExecutionAuthorityCommand) {
				command.ApplyTargetAgentID = "agent_target"
			},
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			store, mock := testExecutionAuthorityStore(t)
			command := valid
			testCase.mutate(&command)

			_, err := store.RecheckExecutionAuthority(context.Background(), command)

			require.ErrorIs(t, err, ErrInvariant)
			require.NotErrorIs(t, err, ErrNotImplemented)
			var applyErr *ApplyError
			require.False(t, errors.As(err, &applyErr),
				"internal command invariants must remain retryable")
			require.NoError(t, mock.ExpectationsWereMet(),
				"malformed commands must fail before querying Core")
		})
	}
}

func TestCoreSubmissionStoreRequiresConfiguredAuthorityDatabase(t *testing.T) {
	_, err := (CoreSubmissionStore{}).RecheckExecutionAuthority(
		context.Background(),
		testExecutionAuthorityCommand(testExecutionAuthorityPayload(ActionCreate)),
	)

	require.Error(t, err)
	require.NotErrorIs(t, err, ErrNotImplemented)
	var applyErr *ApplyError
	require.False(t, errors.As(err, &applyErr),
		"configuration errors must remain retryable")
}

func TestCoreSubmissionStoreRechecksCurrentAuthorityAgainstTiDB(t *testing.T) {
	ctx := context.Background()
	db := roleServiceTiDB.SerializedRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	for index, testCase := range []struct {
		name      string
		action    RoleRequestAction
		arrange   func(testing.TB, coredb.QueryExecer, roleSubmissionFixture) string
		wantAllow bool
	}{
		{
			name:      "baseline create",
			action:    ActionCreate,
			wantAllow: true,
		},
		{
			name:      "baseline update",
			action:    ActionUpdate,
			wantAllow: true,
		},
		{
			name:      "baseline delete",
			action:    ActionDelete,
			wantAllow: true,
		},
		{
			name:   "baseline apply",
			action: ActionApply,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				return seedExecutionAuthorityTarget(t, db, fixture, "apply")
			},
			wantAllow: true,
		},
		{
			name:   "same Human machine rebind",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`,
					fixture.SameManagerMachID, fixture.WorkspaceID, fixture.ApplicantAgentID)
				return ""
			},
			wantAllow: true,
		},
		{
			name:   "applicant member drift",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE agents SET member_id=$1 WHERE workspace_id=$2 AND id=$3`,
					fixture.OtherMemberID, fixture.WorkspaceID, fixture.ApplicantAgentID)
				return ""
			},
		},
		{
			name:   "applicant archived",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE agents SET status='archived' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, fixture.ApplicantAgentID)
				return ""
			},
		},
		{
			name:   "applicant member inactive",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE members SET status='inactive' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, fixture.ApplicantMemberID)
				return ""
			},
		},
		{
			name:   "applicant member is not Agent",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE members SET type='human', user_id=$1, workspace_role='member'
WHERE workspace_id=$2 AND id=$3`,
					fixture.OtherUserID, fixture.WorkspaceID, fixture.ApplicantMemberID)
				return ""
			},
		},
		{
			name:   "applicant machine inactive",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE machines SET status='inactive' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, fixture.ExpectedMachineID)
				return ""
			},
		},
		{
			name:   "manager Human inactive",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE members SET status='inactive' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, fixture.ManagerMemberID)
				return ""
			},
		},
		{
			name:   "manager member is not Human",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				mustExecRoleService(t, db, `
UPDATE members SET type='agent' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, fixture.ManagerMemberID)
				return ""
			},
		},
		{
			name:   "Approval decider drift",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				return seedExecutionAuthorityOtherHuman(t, db, fixture, "decider")
			},
		},
		{
			name:   "applicant manager drift",
			action: ActionCreate,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				seedExecutionAuthorityOtherHuman(t, db, fixture, "manager")
				mustExecRoleService(t, db, `
UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`,
					fixture.OtherManagerMach, fixture.WorkspaceID, fixture.ApplicantAgentID)
				return ""
			},
		},
		{
			name:   "apply target ownership drift",
			action: ActionApply,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				targetAgentID := seedExecutionAuthorityTarget(t, db, fixture, "target_drift")
				mustExecRoleService(t, db, `
UPDATE agents SET machine_id=$1 WHERE workspace_id=$2 AND id=$3`,
					fixture.OtherManagerMach, fixture.WorkspaceID, targetAgentID)
				return targetAgentID
			},
		},
		{
			name:   "apply target archived",
			action: ActionApply,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				targetAgentID := seedExecutionAuthorityTarget(t, db, fixture, "target_archived")
				mustExecRoleService(t, db, `
UPDATE agents SET status='archived' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, targetAgentID)
				return targetAgentID
			},
		},
		{
			name:   "apply target machine inactive",
			action: ActionApply,
			arrange: func(t testing.TB, db coredb.QueryExecer, fixture roleSubmissionFixture) string {
				targetMachineID := "mac_rolesvc_target_inactive"
				targetAgentID := seedExecutionAuthorityTargetWithMachine(
					t, db, fixture, "target_machine_inactive", targetMachineID,
				)
				mustExecRoleService(t, db, `
UPDATE machines SET status='inactive' WHERE workspace_id=$1 AND id=$2`,
					fixture.WorkspaceID, targetMachineID)
				return targetAgentID
			},
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			fixture := seedRoleSubmissionFixture(
				t, db, fmt.Sprintf("authority_%d", index), now,
			)
			targetOrDeciderID := ""
			if testCase.arrange != nil {
				targetOrDeciderID = testCase.arrange(t, db, fixture)
			}
			command := testExecutionAuthorityCommandForFixture(fixture, testCase.action)
			switch testCase.name {
			case "Approval decider drift":
				command.Approval.DecidedByMemberID = targetOrDeciderID
				command.RequiredManagerMemberID = targetOrDeciderID
			case "baseline apply", "apply target ownership drift",
				"apply target archived", "apply target machine inactive":
				command.Payload.Apply.AgentID = targetOrDeciderID
				command.ApplyTargetAgentID = targetOrDeciderID
			}

			authority, err := (CoreSubmissionStore{DB: db}).
				RecheckExecutionAuthority(ctx, command)

			require.NotErrorIs(t, err, ErrNotImplemented)
			if testCase.wantAllow {
				require.NoError(t, err)
				require.Equal(t, fixture.ManagerUserID, authority.ManagerUserID)
				if testCase.action == ActionApply {
					require.Equal(t, []string{command.ApplyTargetAgentID}, authority.OwnedAgentIDs)
				} else {
					require.Empty(t, authority.OwnedAgentIDs)
				}
				return
			}
			var applyErr *ApplyError
			require.ErrorAs(t, err, &applyErr)
			require.Equal(t, ApplyAuthorityDenied, applyErr.Outcome)
		})
	}
}

func testExecutionAuthorityStore(t testing.TB) (CoreSubmissionStore, sqlmock.Sqlmock) {
	t.Helper()
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	t.Cleanup(func() {
		_ = sqlDB.Close()
	})
	return CoreSubmissionStore{DB: coredb.WrapSQLDB(sqlDB)}, mock
}

func testExecutionAuthorityCommand(payload ApprovalPayload) ExecutionAuthorityCommand {
	targetAgentID := ""
	if payload.Apply != nil {
		targetAgentID = payload.Apply.AgentID
	}
	return ExecutionAuthorityCommand{
		Approval: approval.Instance{
			WorkspaceID:       "ws_authority",
			ApplicantMemberID: "mem_applicant",
			DecidedByMemberID: "mem_manager",
		},
		Payload:                 payload,
		RequiredManagerMemberID: "mem_manager",
		ApplyTargetAgentID:      targetAgentID,
	}
}

func testExecutionAuthorityPayload(action RoleRequestAction) ApprovalPayload {
	payload := ApprovalPayload{
		SchemaVersion:    PayloadSchemaVersion,
		RequestID:        "request_authority",
		ApplicantAgentID: "agent_applicant",
		Action:           action,
	}
	switch action {
	case ActionCreate:
		payload.Create = &RoleCreateProposal{
			Name: "Release manager", Definition: "Manage releases",
		}
	case ActionUpdate:
		name := "Updated release manager"
		payload.Update = &RoleUpdateProposal{
			RoleID: "role_release", ExpectedRoleVersionID: "rolev_1", Name: &name,
		}
	case ActionDelete:
		payload.Delete = &RoleDeleteProposal{
			RoleID: "role_release", ExpectedRoleVersionID: "rolev_1",
		}
	case ActionApply:
		payload.Apply = &RoleApplyProposal{
			RoleID: "role_release", RoleVersionID: "rolev_1", AgentID: "agent_target",
		}
	}
	return payload
}

func testExecutionAuthorityCommandForFixture(
	fixture roleSubmissionFixture,
	action RoleRequestAction,
) ExecutionAuthorityCommand {
	command := testExecutionAuthorityCommand(testExecutionAuthorityPayload(action))
	command.Approval.WorkspaceID = fixture.WorkspaceID
	command.Approval.ApplicantMemberID = fixture.ApplicantMemberID
	command.Approval.DecidedByMemberID = fixture.ManagerMemberID
	command.Payload.ApplicantAgentID = fixture.ApplicantAgentID
	command.RequiredManagerMemberID = fixture.ManagerMemberID
	return command
}

func seedExecutionAuthorityOtherHuman(
	t testing.TB,
	db coredb.QueryExecer,
	fixture roleSubmissionFixture,
	suffix string,
) string {
	t.Helper()
	memberID := "mem_rolesvc_other_human_" + suffix
	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,'member','active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
		memberID, fixture.WorkspaceID, fixture.OtherUserID)
	return memberID
}

func seedExecutionAuthorityTarget(
	t testing.TB,
	db coredb.QueryExecer,
	fixture roleSubmissionFixture,
	suffix string,
) string {
	t.Helper()
	return seedExecutionAuthorityTargetWithMachine(
		t, db, fixture, suffix, fixture.SameManagerMachID,
	)
}

func seedExecutionAuthorityTargetWithMachine(
	t testing.TB,
	db coredb.QueryExecer,
	fixture roleSubmissionFixture,
	suffix string,
	machineID string,
) string {
	t.Helper()
	memberID := "mem_rolesvc_target_" + suffix
	agentID := "agt_rolesvc_target_" + suffix
	if machineID != fixture.SameManagerMachID {
		mustExecRoleService(t, db, `
INSERT INTO machines (
  id,workspace_id,created_by_user_id,name,api_key,status,created_at,updated_at
) VALUES ($1,$2,$3,$1,$1,'active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
			machineID, fixture.WorkspaceID, fixture.ManagerUserID)
	}
	mustExecRoleService(t, db, `
INSERT INTO members (
  id,workspace_id,type,display_name,user_id,workspace_role,status,created_at,updated_at
) VALUES ($1,$2,'agent',$1,NULL,NULL,'active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
		memberID, fixture.WorkspaceID)
	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','',
          CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
		agentID, fixture.WorkspaceID, machineID, memberID)
	return agentID
}
