package approvalapp

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/kbgovernance"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
)

type fakePublicationAuthority struct {
	resolved []kbgovernance.ResolvePublicationApprovalCommand
}

func TestPublicationApprovalProjectsReviewerAndApplicantNotifications(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Second)
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_kb_publication_notifications"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_pub_notify_member", "mem_pub_notify_member", "member", now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_pub_notify_admin", "mem_pub_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 := &fakePublicationAuthority{}
	registry := NewSubjectRegistry(approvals)
	require.NoError(t, registry.Register(NewPublicationSubjectHandler(approvals, authority, db, projector)))
	registry.Seal()
	gateway := NewPublicationApprovalGateway(registry, approvals, db, projector)

	created, err := gateway.SubmitPublication(ctx, kbgovernance.SubmitPublicationCommand{
		ActorPrincipalID: "usr_pub_notify_member",
		Prepared:         publicationPrepared(t, workspaceID, "usr_pub_notify_member", "publish-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,
		publicationReviewerClientNotificationID(created.ApprovalID, "mem_pub_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, publicationReviewerURL(created.ApprovalID), *reviewer.BusinessObjectURL)
	applicantPending, found, err := notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		publicationApplicantPendingClientNotificationID(created.ApprovalID))
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, app.AppNotificationStatusActive, applicantPending.Status)
	require.NotNil(t, applicantPending.BusinessObjectURL)
	require.Equal(t, publicationApplicantURL("kb_1"), *applicantPending.BusinessObjectURL)

	_, err = gateway.DecidePublication(ctx, kbgovernance.DecidePublicationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID, ActorPrincipalID: "usr_pub_notify_admin",
		ExpectedVersion: 1, ClientDecisionID: "publish-notify-decision", Decision: kbgovernance.DecisionApproved,
		Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	reviewer, found, err = notifications.FindAppNotificationByClientID(ctx, workspaceID, provisioned.Installation.ID,
		publicationReviewerClientNotificationID(created.ApprovalID, "mem_pub_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,
		publicationApplicantPendingClientNotificationID(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,
		publicationApplicantResultClientNotificationID(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, publicationApplicantURL("kb_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_publication_published", card.TitleKey)
	require.Contains(t, cardJSON, publicationApplicantURL("kb_1"))
}

func (*fakePublicationAuthority) PreparePublication(context.Context, kbgovernance.PreparePublicationCommand) (kbgovernance.PreparedPublication, error) {
	return kbgovernance.PreparedPublication{}, nil
}

func (*fakePublicationAuthority) RecordPublicationApproval(_ context.Context, command kbgovernance.RecordPublicationApprovalCommand) (kbgovernance.PublicationState, error) {
	return kbgovernance.PublicationState{
		RequestID: command.RequestID, ApprovalID: command.ApprovalID,
		ApprovalVersion: command.ApprovalVersion, Status: "pending_approval", DomainVersion: 1,
	}, nil
}

func (authority *fakePublicationAuthority) ResolvePublicationApproval(_ context.Context, command kbgovernance.ResolvePublicationApprovalCommand) (kbgovernance.PublicationState, error) {
	authority.resolved = append(authority.resolved, command)
	status := "rejected"
	if command.Decision == kbgovernance.DecisionApproved {
		status = "published"
	} else if command.Decision == kbgovernance.DecisionWithdrawn {
		status = "withdrawn"
	}
	return kbgovernance.PublicationState{
		ApprovalID: command.ApprovalID, ApprovalVersion: command.ApprovalVersion,
		Status: status, DomainVersion: 2,
	}, nil
}

func TestPublicationApprovalGatewayUsesDerivedApplicantAndReviewer(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Second)
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_kb_publication"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_pub_member", "mem_pub_member", "member", now)
	seedWikiApprovalHuman(t, db, workspaceID, "usr_pub_admin", "mem_pub_admin", "admin", now)

	approvals := approval.New(db)
	authority := &fakePublicationAuthority{}
	registry := NewSubjectRegistry(approvals)
	require.NoError(t, registry.Register(NewPublicationSubjectHandler(approvals, authority, db, nil)))
	registry.Seal()
	gateway := NewPublicationApprovalGateway(registry, approvals, db, nil)
	prepared := publicationPrepared(t, workspaceID, "usr_pub_member", "publish-1", now)

	created, err := gateway.SubmitPublication(ctx, kbgovernance.SubmitPublicationCommand{
		ActorPrincipalID: "usr_pub_member", Prepared: prepared,
	})
	require.NoError(t, err)
	require.Equal(t, "pending_approval", created.Status)
	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_pub_admin"}, assignmentMembers)
	review, err := gateway.InspectPublication(ctx, kbgovernance.InspectPublicationApprovalCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID, ActorPrincipalID: "usr_pub_admin",
	})
	require.NoError(t, err)
	require.True(t, review.CanDecide)
	require.ElementsMatch(t, []kbgovernance.PublicationApprover{
		{PrincipalID: "usr_approval_bot_admin", DisplayName: "mem_approval_bot_admin"},
		{PrincipalID: "usr_pub_admin", DisplayName: "mem_pub_admin"},
	}, review.Approvers)
	applicantReview, err := gateway.InspectPublication(ctx, kbgovernance.InspectPublicationApprovalCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID, ActorPrincipalID: "usr_pub_member",
	})
	require.NoError(t, err)
	require.False(t, applicantReview.CanDecide)
	require.Equal(t, review.Approvers, applicantReview.Approvers)

	decided, err := gateway.DecidePublication(ctx, kbgovernance.DecidePublicationCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID, ActorPrincipalID: "usr_pub_admin",
		ExpectedVersion: 1, ClientDecisionID: "publish-decision-1",
		Decision: kbgovernance.DecisionApproved, Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, "published", decided.Status)
	require.Len(t, authority.resolved, 1)
	require.Equal(t, "usr_pub_admin", authority.resolved[0].ActorPrincipalID)
	resolvedReview, err := gateway.InspectPublication(ctx, kbgovernance.InspectPublicationApprovalCommand{
		WorkspaceID: workspaceID, ApprovalID: created.ApprovalID, ActorPrincipalID: "usr_pub_admin",
	})
	require.NoError(t, err)
	require.False(t, resolvedReview.CanDecide)
	require.Equal(t, review.Approvers, resolvedReview.Approvers)
}

func publicationPrepared(t *testing.T, workspaceID, requesterID, idempotencyKey string, now time.Time) kbgovernance.PreparedPublication {
	t.Helper()
	snapshot := []byte(`{"knowledgeBaseId":"kb_1","workingRevision":"kbr_1","sources":[]}`)
	contentDigest := sha256.Sum256(snapshot)
	payload := kbgovernance.PublicationPayload{
		SchemaVersion: kbgovernance.PublicationPayloadSchema, WorkspaceID: workspaceID,
		KnowledgeBaseID: "kb_1", WorkingRevision: "kbr_1", ContentDigest: hex.EncodeToString(contentDigest[:]),
		RequesterID: requesterID, IdempotencyKey: idempotencyKey,
	}
	raw, digest, err := kbgovernance.EncodePayload(payload)
	require.NoError(t, err)
	return kbgovernance.PreparedPublication{
		Payload: payload, PayloadJSON: raw, PayloadDigest: digest,
		SnapshotJSON: snapshot, CorrelationID: "corr-publication", PreparedAt: now,
	}
}
