package approvalapp

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/role/execution"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
)

type recordingRoleManagementConsumer struct {
	calls         int
	submitCommand execution.SubmitCommand
	replayCommand execution.SubmitCommand
	decideParams  approval.DecideParams
	approvals     map[string]approval.Instance
	assignments   map[string][]approval.Assignment
}

type roleManagementNotificationStore struct {
	NotificationStore
	creates int
}

func (store *roleManagementNotificationStore) CreateAppNotificationForBuiltin(
	context.Context,
	string,
	string,
	app.AppNotificationCommand,
) (app.AppNotificationResult, error) {
	store.creates++
	return app.AppNotificationResult{}, nil
}

func (consumer *recordingRoleManagementConsumer) Submit(
	_ context.Context,
	command execution.SubmitCommand,
) (execution.RequestResult, error) {
	consumer.calls++
	consumer.submitCommand = command
	return execution.RequestResult{
		ApprovalID: "appr_role", ApplicantAgentID: command.Principal.AgentID,
		RequestID: command.RequestID, Status: execution.RequestPending,
	}, nil
}

func (consumer *recordingRoleManagementConsumer) LookupReplay(
	_ context.Context,
	command execution.SubmitCommand,
) (execution.RequestResult, bool, error) {
	consumer.calls++
	consumer.replayCommand = command
	return execution.RequestResult{
		ApprovalID: "appr_role", ApplicantAgentID: command.Principal.AgentID,
		RequestID: command.RequestID, Status: execution.RequestPending,
	}, true, nil
}

func (consumer *recordingRoleManagementConsumer) Decide(
	_ context.Context,
	params approval.DecideParams,
) (execution.RequestResult, error) {
	consumer.calls++
	consumer.decideParams = params
	return execution.RequestResult{
		ApprovalID: params.ApprovalID, Status: execution.RequestProcessing,
	}, nil
}

func roleSubjectPrincipal() SubjectPrincipal {
	return SubjectPrincipal{
		ActorType: "agent", WorkspaceID: "ws_1", PrincipalID: "agt_1",
		MemberID: "mem_1", MachineID: "mac_1",
	}
}

func TestRoleManagementSubjectSubmitReplayAndDecideAreThinAdapters(t *testing.T) {
	ctx := context.Background()
	db := approvalAppTiDB.SerializedRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	workspaceID := "wsp_role_subject"
	applicantID := "mem_role_subject_agent"
	approverID := "mem_role_subject_human"
	seedRoleSubjectActors(t, db, workspaceID, applicantID, approverID, now)
	store := approval.New(db)
	rawPayload := []byte(
		`{"schemaVersion":1,"requestId":"req_1","applicantAgentId":"agt_1","action":"create","create":{"Definition":"Review releases","Description":"","Name":"Release"}}`,
	)
	var expectedPayload any
	require.NoError(t, json.Unmarshal(rawPayload, &expectedPayload))
	rawPayload, err := json.Marshal(expectedPayload)
	require.NoError(t, err)
	payloadSum := sha256.Sum256(rawPayload)
	instance, assignments, created, err := store.Create(ctx, approval.CreateParams{
		ID: "appr_role", WorkspaceID: workspaceID,
		SubjectType: execution.SubjectType, SubjectRef: "ref_role",
		SubjectVersion:      1,
		Payload:             rawPayload,
		PayloadDigest:       hex.EncodeToString(payloadSum[:]),
		IdempotencyKey:      "role-subject-idempotency",
		ApplicantMemberID:   applicantID,
		AllowAgentApplicant: true,
		ApproverMemberIDs:   []string{approverID},
		Now:                 now,
	})
	require.NoError(t, err)
	require.True(t, created)
	require.Len(t, assignments, 1)
	consumer := &recordingRoleManagementConsumer{}
	handler := NewRoleManagementSubjectHandler(store, consumer, &Projector{})
	submitter := handler.(SubjectSubmitter)
	replay := handler.(SubjectReplayLookup)

	decoded, err := submitter.DecodeSubmitPayload(json.RawMessage(
		`{"action":"create","create":{"name":"Release","definition":"Review releases"}}`,
	))
	require.NoError(t, err)
	command := SubmitCommand{
		Principal: SubjectPrincipal{
			ActorType: "agent", WorkspaceID: workspaceID,
			PrincipalID: "agt_1", MemberID: applicantID, MachineID: "mac_1",
		}, ClientRequestID: "req_1",
		Payload: decoded, CorrelationID: "corr_1", Now: time.Unix(100, 0).UTC(),
	}
	submitted, err := submitter.Submit(context.Background(), command)
	require.NoError(t, err)
	require.Equal(t, "appr_role", submitted.Instance.ID)
	require.Equal(t, agent.AuthenticatedAgent{
		WorkspaceID: workspaceID, AgentID: "agt_1", MemberID: applicantID, MachineID: "mac_1",
	}, consumer.submitCommand.Principal)
	require.Equal(t, execution.RoleCreateProposal{
		Name: "Release", Definition: "Review releases",
	}, consumer.submitCommand.Proposal)

	replayed, found, err := replay.LookupReplay(context.Background(), command)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "appr_role", replayed.Instance.ID)
	require.Equal(t, "mac_1", consumer.replayCommand.Principal.MachineID)

	decided, err := handler.Decide(context.Background(), DecisionCommand{
		WorkspaceID: workspaceID, ApprovalID: "appr_role", AssignmentID: assignments[0].ID,
		ActorMemberID: approverID, ExpectedVersion: 1,
		ClientDecisionID: "decision_1", Decision: approval.DecisionApproved,
		Now: time.Unix(200, 0).UTC(),
	})
	require.NoError(t, err)
	require.Equal(t, "appr_role", decided.Instance.ID)
	require.Equal(t, "decision_1", consumer.decideParams.ClientDecisionID)
	require.Equal(t, instance.ID, submitted.Instance.ID)
}

func TestRoleManagementSubjectStrictDecodeAndMissingMachineFailClosed(t *testing.T) {
	consumer := &recordingRoleManagementConsumer{}
	handler := NewRoleManagementSubjectHandler(nil, consumer, nil)
	submitter := handler.(SubjectSubmitter)
	replay := handler.(SubjectReplayLookup)

	for _, raw := range []string{
		`{"action":"create","create":{"name":"Release","definition":"Review"},"unknown":true}`,
		`{"action":"create","create":{"name":"Release","definition":"Review"},"delete":{"roleId":"role_1","expectedRoleVersionId":"v1"}}`,
		`{"action":"unknown"}`,
		`{"action":"create","create":{"name":"Release","definition":"Review"}} trailing`,
	} {
		_, err := submitter.DecodeSubmitPayload(json.RawMessage(raw))
		require.Error(t, err, raw)
	}

	decoded, err := submitter.DecodeSubmitPayload(json.RawMessage(
		`{"action":"delete","delete":{"roleId":"role_1","expectedRoleVersionId":"v1"}}`,
	))
	require.NoError(t, err)
	command := SubmitCommand{
		Principal: SubjectPrincipal{
			ActorType: "agent", WorkspaceID: "ws_1", PrincipalID: "agt_1", MemberID: "mem_1",
		},
		ClientRequestID: "req_1", Payload: decoded,
	}
	_, err = submitter.Submit(context.Background(), command)
	require.Error(t, err)
	_, _, err = replay.LookupReplay(context.Background(), command)
	require.Error(t, err)
	require.Empty(t, consumer.submitCommand.Principal.AgentID)
	require.Empty(t, consumer.replayCommand.Principal.AgentID)
}

func TestRoleManagementSubjectValidateAndPolicies(t *testing.T) {
	handler := NewRoleManagementSubjectHandler(
		&approval.Store{}, &recordingRoleManagementConsumer{}, &Projector{},
	)
	require.Equal(t, execution.SubjectType, handler.SubjectType())
	require.Equal(t, SubjectCancelNotSupported, handler.CancelPolicy())
	require.Equal(t, SubjectReconcileNone, handler.ReconcilePolicy())

	valid := approval.Instance{
		WorkspaceID: "ws_1", SubjectType: execution.SubjectType,
		SubjectVersion: 1, ApplicantMemberID: "mem_1",
		Payload: []byte(`{"schemaVersion":1,"requestId":"req_1","applicantAgentId":"agt_1","action":"create","create":{"Name":"Release","Definition":"Review","Description":""}}`),
	}
	idempotencyKey, err := execution.RoleIdempotencyKey("ws_1", "agt_1", "req_1")
	require.NoError(t, err)
	valid.SubjectRef, valid.IdempotencyKey = idempotencyKey, idempotencyKey
	var resolved any
	require.NoError(t, json.Unmarshal(valid.Payload, &resolved))
	valid.Payload, _ = json.Marshal(resolved)
	sum := sha256.Sum256(valid.Payload)
	valid.PayloadDigest = hex.EncodeToString(sum[:])
	require.NoError(t, handler.Validate(context.Background(), valid))
	valid.SubjectType = "other"
	require.Error(t, handler.Validate(context.Background(), valid))
}

func TestRoleManagementSubjectAndCardRejectNonDurablePayload(t *testing.T) {
	ctx := context.Background()
	db := approvalAppTiDB.SerializedRollbackDB(t, ctx)
	seedApprovalAppWorkspace(t, db, "ws_1", time.Now().UTC())
	notifications := &roleManagementNotificationStore{}
	consumer := &recordingRoleManagementConsumer{}
	handler := NewRoleManagementSubjectHandler(
		approval.New(db),
		consumer,
		NewProjector(notifications, NewProvisioner(db)),
	)
	presenter := handler.(SubjectPendingPresenter)
	idempotencyKey, err := execution.RoleIdempotencyKey("ws_1", "agt_1", "req_1")
	require.NoError(t, err)
	resolved := []byte(
		`{"action":"create","applicantAgentId":"agt_1","create":{"Definition":"Review","Description":"","Name":"Release"},"requestId":"req_1","schemaVersion":1}`,
	)
	sum := sha256.Sum256(resolved)
	base := approval.Instance{
		ID: "appr_role", WorkspaceID: "ws_1", SubjectType: execution.SubjectType,
		SubjectRef: idempotencyKey, SubjectVersion: 1,
		IdempotencyKey: idempotencyKey, ApplicantMemberID: "mem_1",
		State: approval.StatePending, Version: 1,
		Payload: resolved, PayloadDigest: hex.EncodeToString(sum[:]),
	}
	for _, testCase := range []struct {
		name   string
		mutate func(instance *approval.Instance)
	}{
		{
			name: "duplicate action",
			mutate: func(instance *approval.Instance) {
				instance.Payload = append(
					append(append([]byte(nil), instance.Payload[:len(instance.Payload)-1]...),
						[]byte(`,"action":"create"`)...),
					'}',
				)
			},
		},
		{
			name: "pretty raw digest",
			mutate: func(instance *approval.Instance) {
				var pretty bytes.Buffer
				require.NoError(t, json.Indent(&pretty, instance.Payload, "", "  "))
				instance.Payload = pretty.Bytes()
				rawSum := sha256.Sum256(instance.Payload)
				instance.PayloadDigest = hex.EncodeToString(rawSum[:])
			},
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			instance := base
			testCase.mutate(&instance)

			validateErr := handler.Validate(ctx, instance)
			_, cardErr := presenter.PresentPending(ctx, SubmitResult{
				Instance: instance,
				Assignments: []approval.Assignment{{
					ID: "asg_1", WorkspaceID: instance.WorkspaceID,
					ApprovalInstanceID: instance.ID, ApproverMemberID: "mem_manager",
					Status: approval.AssignmentPending,
				}},
			})
			require.Error(t, validateErr)
			require.Error(t, cardErr)
			require.Zero(t, consumer.calls,
				"invalid durable payload must not reach the Role consumer")
			require.Zero(t, notifications.creates,
				"invalid durable payload must not produce card content")
		})
	}
}

func TestRoleManagementCardEntryRejectsMalformedDurableUnionWithoutPanic(t *testing.T) {
	idempotencyKey, err := execution.RoleIdempotencyKey("ws_1", "agt_1", "req_1")
	require.NoError(t, err)
	instance := approval.Instance{
		WorkspaceID: "ws_1", SubjectType: execution.SubjectType,
		SubjectRef: idempotencyKey, SubjectVersion: 1,
		IdempotencyKey: idempotencyKey, ApplicantMemberID: "mem_1",
		Payload: []byte(
			`{"schemaVersion":1,"requestId":"req_1","applicantAgentId":"agt_1","action":"create"}`,
		),
	}
	sum := sha256.Sum256(instance.Payload)
	instance.PayloadDigest = hex.EncodeToString(sum[:])

	var cardErr error
	require.NotPanics(t, func() {
		payload, decodeErr := decodeRoleManagementPayload(instance)
		cardErr = decodeErr
		if cardErr == nil {
			_, cardErr = roleManagementCardContent(
				approval.Instance{ID: "appr_1", Version: 1},
				"asg_1", "mem_manager", payload, approval.StatePending,
			)
		}
	})
	require.Error(t, cardErr, "card entry must stop before create summary dereference")
}

func TestRoleManagementCardContentPendingAndResolved(t *testing.T) {
	instance := approval.Instance{
		ID: "appr_role", Version: 1, State: approval.StatePending,
	}
	payload := execution.ApprovalPayload{
		SchemaVersion: 1, RequestID: "req_1", ApplicantAgentID: "agt_1",
		Action: execution.ActionApply,
		Apply: &execution.RoleApplyProposal{
			RoleID: "role_1", RoleVersionID: "rlv_1", AgentID: "agt_target",
		},
	}
	pending, err := roleManagementCardContent(
		instance, "asg_1", "mem_manager", payload, approval.StatePending,
	)
	require.NoError(t, err)
	var card map[string]any
	require.NoError(t, json.Unmarshal(pending.CardJSON, &card))
	require.Equal(t, "approval_action", card["template"])
	require.Equal(t, approval.StatePending, card["status"])
	require.Len(t, card["actions"], 2)
	require.Equal(t, map[string]any{
		"action": "apply",
		"summary": map[string]any{
			"roleId": "role_1", "roleVersionId": "rlv_1", "agentId": "agt_target",
		},
	}, card["proposal"])

	instance.Version = 2
	instance.State = approval.StateApproved
	resolved, err := roleManagementCardContent(
		instance, "asg_1", "", payload, roleManagementCardStatusProcessing,
	)
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(resolved.CardJSON, &card))
	require.Equal(t, roleManagementCardStatusProcessing, card["status"])
	require.Empty(t, card["actions"])
}

func seedRoleSubjectActors(
	t testing.TB,
	db coredb.QueryExecer,
	workspaceID, applicantID, approverID string,
	now time.Time,
) {
	t.Helper()
	userID := "usr_" + approverID
	orgID := "org_" + workspaceID
	mustExec := func(query string, args ...any) {
		_, err := db.Exec(context.Background(), query, args...)
		require.NoError(t, err)
	}
	mustExec(`
INSERT INTO users (id,email,name,display_name,password_hash,created_at,updated_at)
VALUES ($1,$2,$1,$1,'hash',$3,$3)`,
		userID, userID+"@example.com", now)
	mustExec(`
INSERT INTO organizations (id,slug,name,created_by_user_id,created_at,updated_at)
VALUES ($1,$1,$1,$2,$3,$3)`, orgID, userID, now)
	mustExec(`
INSERT INTO workspaces (id,organization_id,slug,name,created_at,updated_at)
VALUES ($1,$2,$1,$1,$3,$3)`, workspaceID, orgID, now)
	mustExec(`
INSERT INTO members (
  id,workspace_id,type,display_name,user_id,workspace_role,status,created_at,updated_at
) VALUES
($1,$3,'agent',$1,NULL,NULL,'active',$5,$5),
($2,$3,'human',$2,$4,'owner','active',$5,$5)`,
		applicantID, approverID, workspaceID, userID, now)
}
