package approvalapp

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

	"github.com/stretchr/testify/require"

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

type fakeWikiGenerationAuthority struct {
	mu             sync.Mutex
	recorded       []kbgovernance.RecordWikiApprovalCommand
	resolved       []kbgovernance.ResolveWikiApprovalCommand
	staleOnApprove bool
}

func TestWikiGenerationApprovalProjectsReviewerAndApplicantNotifications(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Second)
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_kb_wiki_notifications"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_wiki_notify_member", "mem_wiki_notify_member", "member", now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_wiki_notify_admin", "mem_wiki_notify_admin", "admin", now)

	notifications := app.New(db, app.Options{})
	projector := NewProjector(notifications, NewProvisioner(db))
	require.NoError(t, projector.Register())
	approvals := approval.New(db)
	authority := &fakeWikiGenerationAuthority{}
	registry := NewSubjectRegistry(approvals)
	require.NoError(t, registry.Register(NewWikiGenerationSubjectHandler(approvals, authority, db, projector)))
	registry.Seal()
	gateway := NewWikiGenerationApprovalGateway(registry, approvals, db, projector)

	created, err := gateway.SubmitWikiGeneration(ctx, kbgovernance.SubmitWikiGenerationCommand{
		ActorPrincipalID: "usr_wiki_notify_member",
		Prepared:         wikiGenerationPrepared(t, workspaceID, "usr_wiki_notify_member", "wiki-notify", now),
	})
	require.NoError(t, err)
	provisioned, err := projector.provisioner.EnsureWorkspace(ctx, workspaceID, now)
	require.NoError(t, err)
	reviewer, found, err := notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		wikiGenerationReviewerClientNotificationID(created.ApprovalID, "mem_wiki_notify_admin"))
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, app.AppNotificationStatusActive, reviewer.Status)
	require.Equal(t, app.AppNotificationContentInteractiveCard, reviewer.ContentKind)
	require.NotNil(t, reviewer.BusinessObjectURL)
	require.Equal(t, wikiGenerationURL("kb_1", "src_1"), *reviewer.BusinessObjectURL)
	applicantPending, found, err := notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		wikiGenerationApplicantPendingClientNotificationID(created.ApprovalID))
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, app.AppNotificationStatusActive, applicantPending.Status)

	_, err = gateway.DecideWikiGeneration(ctx, kbgovernance.DecideWikiGenerationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID, ActorPrincipalID: "usr_wiki_notify_admin",
		ExpectedVersion: 1, ClientDecisionID: "wiki-notify-decision", Decision: kbgovernance.DecisionApproved,
		Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	reviewer, found, err = notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		wikiGenerationReviewerClientNotificationID(created.ApprovalID, "mem_wiki_notify_admin"))
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, app.AppNotificationStatusResolved, reviewer.Status)
	applicantPending, found, err = notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		wikiGenerationApplicantPendingClientNotificationID(created.ApprovalID))
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, app.AppNotificationStatusResolved, applicantPending.Status)
	applicantResult, found, err := notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		wikiGenerationApplicantResultClientNotificationID(created.ApprovalID))
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, app.AppNotificationStatusActive, applicantResult.Status)
	require.NotNil(t, applicantResult.BusinessObjectURL)
	require.Equal(t, wikiGenerationURL("kb_1", "src_1"), *applicantResult.BusinessObjectURL)
	var cardJSON string
	require.NoError(t, db.QueryRow(ctx, `
SELECT render_payload_json
FROM service_notification_cards
WHERE service_notification_id = $1
LIMIT 1
`, applicantResult.ID).Scan(&cardJSON))
	var card struct {
		TitleKey string `json:"titleKey"`
	}
	require.NoError(t, json.Unmarshal([]byte(cardJSON), &card))
	require.Equal(t, "kb_wiki_generation_queued", card.TitleKey)
}

func (*fakeWikiGenerationAuthority) PrepareWikiGeneration(context.Context, kbgovernance.PrepareWikiGenerationCommand) (kbgovernance.PreparedWikiGeneration, error) {
	return kbgovernance.PreparedWikiGeneration{}, nil
}

func (authority *fakeWikiGenerationAuthority) RecordWikiGenerationApproval(_ context.Context, command kbgovernance.RecordWikiApprovalCommand) (kbgovernance.WikiGenerationState, error) {
	authority.mu.Lock()
	defer authority.mu.Unlock()
	authority.recorded = append(authority.recorded, command)
	return kbgovernance.WikiGenerationState{
		RequestID: command.RequestID, ApprovalID: command.ApprovalID,
		ApprovalVersion: command.ApprovalVersion, Status: "pending_approval", DomainVersion: 1,
	}, nil
}

func (authority *fakeWikiGenerationAuthority) ResolveWikiGenerationApproval(_ context.Context, command kbgovernance.ResolveWikiApprovalCommand) (kbgovernance.WikiGenerationState, error) {
	authority.mu.Lock()
	defer authority.mu.Unlock()
	authority.resolved = append(authority.resolved, command)
	status := "rejected"
	if command.Decision == kbgovernance.DecisionApproved {
		status = "queued"
	} else if command.Decision == kbgovernance.DecisionWithdrawn {
		status = "withdrawn"
	}
	return kbgovernance.WikiGenerationState{
		ApprovalID: command.ApprovalID, ApprovalVersion: command.ApprovalVersion,
		Status: status, DomainVersion: 2,
		Stale: authority.staleOnApprove && command.Decision == kbgovernance.DecisionApproved,
	}, nil
}

func TestWikiGenerationApprovalGatewayCancelsApprovedStaleSubject(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Second)
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_kb_wiki_stale"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_kb_member", "mem_kb_member", "member", now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_kb_admin", "mem_kb_admin", "admin", now)

	approvals := approval.New(db)
	authority := &fakeWikiGenerationAuthority{staleOnApprove: true}
	registry := NewSubjectRegistry(approvals)
	require.NoError(t, registry.Register(NewWikiGenerationSubjectHandler(approvals, authority, db, nil)))
	registry.Seal()
	gateway := NewWikiGenerationApprovalGateway(registry, approvals, db, nil)
	created, err := gateway.SubmitWikiGeneration(ctx, kbgovernance.SubmitWikiGenerationCommand{
		ActorPrincipalID: "usr_kb_member",
		Prepared:         wikiGenerationPrepared(t, workspaceID, "usr_kb_member", "request-stale", now),
	})
	require.NoError(t, err)
	command := kbgovernance.DecideWikiGenerationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID,
		ActorPrincipalID: "usr_kb_admin", ExpectedVersion: 1,
		ClientDecisionID: "decision-stale", Decision: kbgovernance.DecisionApproved, Now: now.Add(time.Minute),
	}
	state, err := gateway.DecideWikiGeneration(ctx, command)
	require.NoError(t, err)
	require.True(t, state.Stale)
	instance, err := approvals.Load(ctx, workspaceID, created.ApprovalID)
	require.NoError(t, err)
	require.Equal(t, approval.StateCancelled, instance.State)
	require.Equal(t, "stale_subject", instance.DecisionReason)
	require.Equal(t, int64(3), instance.Version)

	_, err = gateway.DecideWikiGeneration(ctx, command)
	require.NoError(t, err)
}

func TestWikiGenerationApprovalGatewaySubmitsAndDecidesWithDerivedMembers(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Second)
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_kb_wiki_gateway"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_kb_member", "mem_kb_member", "member", now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_kb_admin", "mem_kb_admin", "admin", now)

	approvals := approval.New(db)
	authority := &fakeWikiGenerationAuthority{}
	registry := NewSubjectRegistry(approvals)
	require.NoError(t, registry.Register(NewWikiGenerationSubjectHandler(approvals, authority, db, nil)))
	registry.Seal()
	gateway := NewWikiGenerationApprovalGateway(registry, approvals, db, nil)
	prepared := wikiGenerationPrepared(t, workspaceID, "usr_kb_member", "request-1", now)

	created, err := gateway.SubmitWikiGeneration(ctx, kbgovernance.SubmitWikiGenerationCommand{
		ActorPrincipalID: "usr_kb_member", Prepared: prepared,
	})
	require.NoError(t, err)
	require.Equal(t, "pending_approval", created.Status)
	require.Equal(t, int64(1), created.ApprovalVersion)

	replayed, err := gateway.SubmitWikiGeneration(ctx, kbgovernance.SubmitWikiGenerationCommand{
		ActorPrincipalID: "usr_kb_member", Prepared: prepared,
	})
	require.NoError(t, err)
	require.Equal(t, created.ApprovalID, replayed.ApprovalID)
	assignments, err := approvals.ListAssignments(ctx, workspaceID, created.ApprovalID)
	require.NoError(t, err)
	assignmentMembers := make([]string, 0, len(assignments))
	for _, assignment := range assignments {
		assignmentMembers = append(assignmentMembers, assignment.ApproverMemberID)
	}
	require.ElementsMatch(t, []string{"mem_approval_bot_admin", "mem_kb_admin"}, assignmentMembers)

	decided, err := gateway.DecideWikiGeneration(ctx, kbgovernance.DecideWikiGenerationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID,
		ActorPrincipalID: "usr_kb_admin", ExpectedVersion: 1,
		ClientDecisionID: "decision-1", Decision: kbgovernance.DecisionApproved, Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, "queued", decided.Status)
	require.Equal(t, int64(2), decided.ApprovalVersion)
	instance, err := approvals.Load(ctx, workspaceID, created.ApprovalID)
	require.NoError(t, err)
	require.Equal(t, approval.StateApproved, instance.State)
	require.Equal(t, int64(2), instance.Version)
	require.Len(t, authority.resolved, 1)
	require.Equal(t, "usr_kb_admin", authority.resolved[0].ActorPrincipalID)
}

func TestWikiGenerationApprovalGatewayWithdrawsOnlyAsApplicant(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Second)
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_kb_wiki_withdraw"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_kb_member", "mem_kb_member", "member", now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_kb_admin", "mem_kb_admin", "owner", now)

	approvals := approval.New(db)
	authority := &fakeWikiGenerationAuthority{}
	registry := NewSubjectRegistry(approvals)
	require.NoError(t, registry.Register(NewWikiGenerationSubjectHandler(approvals, authority, db, nil)))
	registry.Seal()
	gateway := NewWikiGenerationApprovalGateway(registry, approvals, db, nil)
	created, err := gateway.SubmitWikiGeneration(ctx, kbgovernance.SubmitWikiGenerationCommand{
		ActorPrincipalID: "usr_kb_member",
		Prepared:         wikiGenerationPrepared(t, workspaceID, "usr_kb_member", "request-withdraw", now),
	})
	require.NoError(t, err)

	_, err = gateway.WithdrawWikiGeneration(ctx, kbgovernance.WithdrawWikiGenerationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID,
		ActorPrincipalID: "usr_kb_admin", ExpectedVersion: 1,
		ClientRequestID: "withdraw-wrong", Now: now.Add(time.Minute),
	})
	require.Error(t, err)

	withdrawn, err := gateway.WithdrawWikiGeneration(ctx, kbgovernance.WithdrawWikiGenerationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID,
		ActorPrincipalID: "usr_kb_member", ExpectedVersion: 1,
		ClientRequestID: "withdraw-1", Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, "withdrawn", withdrawn.Status)
	require.Equal(t, int64(2), withdrawn.ApprovalVersion)
}

func seedWikiApprovalHuman(t *testing.T, db coredb.DB, workspaceID, userID, memberID, role string, now time.Time) {
	t.Helper()
	mustExec(t, db, `
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(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,$4,'active',$5,$5)
`, memberID, workspaceID, userID, role, now)
}

func wikiGenerationPrepared(t *testing.T, workspaceID, requesterID, idempotencyKey string, now time.Time) kbgovernance.PreparedWikiGeneration {
	t.Helper()
	payload := kbgovernance.WikiGenerationPayload{
		SchemaVersion: kbgovernance.WikiGenerationPayloadSchema,
		WorkspaceID:   workspaceID, KnowledgeBaseID: "kb_1", SourceID: "src_1",
		SourceRevision: "rev_1", SourceCommitSHA: "commit-1", Scope: "full", Language: "zh-CN",
		RequesterID: requesterID, IdempotencyKey: idempotencyKey,
	}
	raw, digest, err := kbgovernance.EncodePayload(payload)
	require.NoError(t, err)
	return kbgovernance.PreparedWikiGeneration{
		Payload: payload, PayloadJSON: raw, PayloadDigest: digest,
		CorrelationID: "corr-1", PreparedAt: now,
	}
}
