package approvalapp

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

type fakeSkillDeleteApprovalStore struct {
	instance    approval.Instance
	assignments []approval.Assignment
	loadErr     error
	createCalls int
	decideCalls int
}

func (store *fakeSkillDeleteApprovalStore) LoadByIdempotencyKey(
	_ context.Context,
	workspaceID string,
	applicantMemberID string,
	idempotencyKey string,
) (approval.Instance, error) {
	if store.loadErr != nil {
		return approval.Instance{}, store.loadErr
	}
	if store.instance.ID == "" ||
		store.instance.WorkspaceID != workspaceID ||
		store.instance.ApplicantMemberID != applicantMemberID ||
		store.instance.IdempotencyKey != idempotencyKey {
		return approval.Instance{}, storeerr.ErrNotFound
	}
	return store.instance, nil
}

func (store *fakeSkillDeleteApprovalStore) Create(
	_ context.Context,
	params approval.CreateParams,
) (approval.Instance, []approval.Assignment, bool, error) {
	store.createCalls++
	if store.instance.ID != "" {
		return store.instance, store.assignments, false, nil
	}
	store.instance = approval.Instance{
		ID: params.ID, WorkspaceID: params.WorkspaceID,
		SubjectType: params.SubjectType, SubjectRef: params.SubjectRef,
		SubjectVersion: params.SubjectVersion,
		Payload:        params.Payload, PayloadDigest: params.PayloadDigest,
		IdempotencyKey:    params.IdempotencyKey,
		ApplicantMemberID: params.ApplicantMemberID,
		State:             approval.StatePending, Version: 1,
		CorrelationID: params.CorrelationID,
	}
	store.assignments = []approval.Assignment{{
		ID: "apasg_skill", WorkspaceID: params.WorkspaceID,
		ApprovalInstanceID: params.ID,
		ApproverMemberID:   params.ApproverMemberIDs[0],
		Status:             approval.AssignmentPending, Version: 1,
	}}
	store.loadErr = nil
	return store.instance, store.assignments, true, nil
}

func (store *fakeSkillDeleteApprovalStore) ListAssignments(
	context.Context,
	string,
	string,
) ([]approval.Assignment, error) {
	return append([]approval.Assignment(nil), store.assignments...), nil
}

func (store *fakeSkillDeleteApprovalStore) LoadAssignment(
	context.Context,
	string,
	string,
) (approval.Assignment, approval.Instance, error) {
	return store.assignments[0], store.instance, nil
}

func (store *fakeSkillDeleteApprovalStore) Decide(
	_ context.Context,
	params approval.DecideParams,
) (approval.DecisionResult, error) {
	store.decideCalls++
	store.instance.Version++
	store.instance.Decision = params.Decision
	store.instance.DecidedByMemberID = params.ActorMemberID
	store.instance.State = approval.StateRejected
	store.assignments[0].Status = approval.AssignmentRejected
	if params.Decision == approval.DecisionApproved {
		store.instance.State = approval.StateApproved
		store.assignments[0].Status = approval.AssignmentApproved
	}
	return approval.DecisionResult{
		Instance: store.instance, Assignment: store.assignments[0],
	}, nil
}

type fakeSkillDeleteAuthority struct {
	prepared     SkillDeletePrepared
	prepareCalls int
	executeCalls int
	executeErrs  []error
}

func (authority *fakeSkillDeleteAuthority) Prepare(
	context.Context,
	SkillDeleteAuthorityCommand,
) (SkillDeletePrepared, error) {
	authority.prepareCalls++
	return authority.prepared, nil
}

func (authority *fakeSkillDeleteAuthority) Execute(
	context.Context,
	SkillDeleteAuthorityCommand,
) error {
	authority.executeCalls++
	if len(authority.executeErrs) == 0 {
		return nil
	}
	err := authority.executeErrs[0]
	authority.executeErrs = authority.executeErrs[1:]
	return err
}

func skillDeleteTestCommand(skillID string) SubmitCommand {
	return SubmitCommand{
		Principal: SubjectPrincipal{
			ActorType: "agent", WorkspaceID: "wsp_1",
			PrincipalID: "agt_1", MemberID: "mem_agent",
			MachineID: "mac_1",
		},
		ClientRequestID: "req_1",
		Payload:         SkillDeleteRequest{SkillID: skillID},
		CorrelationID:   "corr_1",
		Now:             time.Unix(100, 0).UTC(),
	}
}

func newSkillDeleteTestHandler(
	store *fakeSkillDeleteApprovalStore,
	authority *fakeSkillDeleteAuthority,
) *skillDeleteSubjectHandler {
	return NewSkillDeleteSubjectHandler(
		store, authority, &Projector{},
	).(*skillDeleteSubjectHandler)
}

func TestSkillDeleteSubjectSameRequestPendingAndRejectedNeverExecutes(t *testing.T) {
	store := &fakeSkillDeleteApprovalStore{loadErr: storeerr.ErrNotFound}
	authority := &fakeSkillDeleteAuthority{prepared: SkillDeletePrepared{
		SkillID: "skill_1", SkillName: "Release",
		ApproverMemberID: "mem_owner",
	}}
	handler := newSkillDeleteTestHandler(store, authority)
	command := skillDeleteTestCommand("skill_1")

	first, err := handler.Submit(context.Background(), command)
	require.NoError(t, err)
	require.True(t, first.Created)
	require.Equal(t, SkillDeleteResult{
		ApprovalID: first.Instance.ID, Status: SkillDeletePending,
	}, first.Value)
	require.Equal(t, 1, authority.prepareCalls)
	require.Zero(t, authority.executeCalls)

	replay, err := handler.Submit(context.Background(), command)
	require.NoError(t, err)
	require.False(t, replay.Created)
	require.Equal(t, first.Instance.ID, replay.Instance.ID)
	require.Equal(t, 1, authority.prepareCalls,
		"same requestId must replay before mutable preflight")
	require.Zero(t, authority.executeCalls)

	conflict := command
	conflict.Payload = SkillDeleteRequest{SkillID: "skill_other"}
	_, err = handler.Submit(context.Background(), conflict)
	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.Equal(t, 1, authority.prepareCalls)
	require.Zero(t, authority.executeCalls)

	store.instance.State = approval.StateRejected
	store.instance.Version = 2
	store.assignments[0].Status = approval.AssignmentRejected
	rejected, err := handler.Submit(context.Background(), command)
	require.NoError(t, err)
	require.Equal(t, SkillDeleteRejected,
		rejected.Value.(SkillDeleteResult).Status)
	require.Zero(t, authority.executeCalls)
}

func TestSkillDeleteSubjectReusesGenericApprovalStore(t *testing.T) {
	ctx := context.Background()
	db := approvalAppTiDB.SerializedRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	workspaceID := "wsp_skill_delete_subject"
	seedRoleSubjectActors(
		t, db, workspaceID, "mem_skill_agent", "mem_skill_owner", now,
	)
	authority := &fakeSkillDeleteAuthority{prepared: SkillDeletePrepared{
		SkillID: "skill_1", SkillName: "Release",
		ApproverMemberID: "mem_skill_owner",
	}}
	handler := NewSkillDeleteSubjectHandler(
		approval.New(db), authority, &Projector{},
	).(*skillDeleteSubjectHandler)
	command := skillDeleteTestCommand("skill_1")
	command.Principal.WorkspaceID = workspaceID
	command.Principal.MemberID = "mem_skill_agent"

	first, err := handler.Submit(ctx, command)
	require.NoError(t, err)
	second, err := handler.Submit(ctx, command)
	require.NoError(t, err)

	require.True(t, first.Created)
	require.False(t, second.Created)
	require.Equal(t, first.Instance.ID, second.Instance.ID)
	require.Equal(t, SkillDeleteSubjectType, first.Instance.SubjectType)
	require.Equal(t, approval.StatePending, first.Instance.State)
	require.Len(t, first.Assignments, 1)
	require.Equal(t, "mem_skill_owner",
		first.Assignments[0].ApproverMemberID)
	require.Equal(t, 1, authority.prepareCalls)
	var count int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM approval_instances
WHERE workspace_id=$1 AND subject_type=$2
`, workspaceID, SkillDeleteSubjectType).Scan(&count))
	require.Equal(t, 1, count,
		"Skill delete must reuse generic Approval persistence only")
}

func TestSkillDeleteSubjectApprovedReplayRetriesSynchronousDelete(t *testing.T) {
	store := &fakeSkillDeleteApprovalStore{loadErr: storeerr.ErrNotFound}
	authority := &fakeSkillDeleteAuthority{
		prepared: SkillDeletePrepared{
			SkillID: "skill_1", SkillName: "Release",
			ApproverMemberID: "mem_owner",
		},
		executeErrs: []error{errors.New("upstream response lost"), nil},
	}
	handler := newSkillDeleteTestHandler(store, authority)
	command := skillDeleteTestCommand("skill_1")
	pending, err := handler.Submit(context.Background(), command)
	require.NoError(t, err)

	decided, err := handler.Decide(context.Background(), DecisionCommand{
		WorkspaceID: "wsp_1", ApprovalID: pending.Instance.ID,
		AssignmentID: "apasg_skill", ActorMemberID: "mem_owner",
		ExpectedVersion: 1, ClientDecisionID: "decision_1",
		Decision: approval.DecisionApproved,
	})
	require.ErrorContains(t, err, "response lost")
	require.Equal(t, 1, authority.executeCalls)
	require.Empty(t, decided.Instance.ID)
	require.Equal(t, approval.StateApproved, store.instance.State,
		"the generic Approval commits before the external Resource DELETE")

	deleted, err := handler.Submit(context.Background(), command)
	require.NoError(t, err)
	require.Equal(t, SkillDeleteDeleted,
		deleted.Value.(SkillDeleteResult).Status)
	require.Equal(t, 2, authority.executeCalls,
		"same requestId must retry an approved transport failure")

	replayed, err := handler.Submit(context.Background(), command)
	require.NoError(t, err)
	require.Equal(t, SkillDeleteDeleted,
		replayed.Value.(SkillDeleteResult).Status)
	require.Equal(t, 3, authority.executeCalls,
		"no receipt or fence is invented; an already-missing Resource is idempotent")
}

func TestSkillDeleteSubjectStrictPayloadAndCardContract(t *testing.T) {
	handler := newSkillDeleteTestHandler(
		&fakeSkillDeleteApprovalStore{},
		&fakeSkillDeleteAuthority{},
	)
	submitter := any(handler).(SubjectSubmitter)
	_, err := submitter.DecodeSubmitPayload(
		json.RawMessage(`{"skillId":"skill_1","owner":"usr_1"}`),
	)
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	payload := skillDeletePayload{
		ApplicantAgentID: "agt_1", RequestID: "req_1",
		SchemaVersion: 1, SkillID: "skill_1", SkillName: "Release",
	}
	instance := approval.Instance{
		ID: "appr_1", State: approval.StatePending, Version: 1,
	}
	content, err := skillDeleteCardContent(
		instance, "apasg_1", "mem_owner", payload, approval.StatePending,
	)
	require.NoError(t, err)
	document := decodeApprovalCard(t, content)
	require.Equal(t, "approval_action", document.Template)
	require.Equal(t, "skill_delete_review", document.TitleKey)
	require.Len(t, document.Actions, 2)
	for _, action := range document.Actions {
		require.Equal(t, map[string]any{
			"kind": "members", "memberIds": []any{"mem_owner"},
		}, action["actorPolicy"])
	}
}
