package approvalapp

import (
	"context"
	"fmt"
	"testing"

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

type subjectRegistryTestHandler struct {
	subjectType                                                   string
	requestType                                                   string
	validated, submitted, decided, cancelled, presented, resolved bool
	cancelPolicy                                                  SubjectCancelPolicy
	presentErr                                                    error
}

type subjectRegistryTestLoader struct {
	instance approval.Instance
	marks    map[string]int64
	markErr  error
}

func (loader subjectRegistryTestLoader) MarkProjected(
	_ context.Context,
	_ string,
	approvalID string,
	version int64,
) error {
	if loader.markErr != nil {
		return loader.markErr
	}
	loader.marks[approvalID] = version
	return nil
}

func (loader subjectRegistryTestLoader) Load(
	_ context.Context,
	workspaceID, approvalID string,
) (approval.Instance, error) {
	if loader.instance.WorkspaceID != workspaceID ||
		loader.instance.ID != approvalID {
		return approval.Instance{}, fmt.Errorf("not found")
	}
	return loader.instance, nil
}

func (h *subjectRegistryTestHandler) SubjectType() string { return h.subjectType }
func (h *subjectRegistryTestHandler) RequestType() string {
	if h.requestType != "" {
		return h.requestType
	}
	return h.subjectType
}
func (h *subjectRegistryTestHandler) CancelPolicy() SubjectCancelPolicy {
	if h.cancelPolicy != "" {
		return h.cancelPolicy
	}
	return SubjectCancelByApplicant
}
func (*subjectRegistryTestHandler) ReconcilePolicy() SubjectReconcilePolicy {
	return SubjectReconcileProjection
}
func (h *subjectRegistryTestHandler) Validate(context.Context, approval.Instance) error {
	h.validated = true
	return nil
}
func (h *subjectRegistryTestHandler) Submit(_ context.Context, command SubmitCommand) (SubmitResult, error) {
	h.submitted = true
	return SubmitResult{Instance: approval.Instance{SubjectType: command.SubjectType}}, nil
}
func (*subjectRegistryTestHandler) DecodeSubmitPayload(payload any) (any, error) {
	return payload, nil
}
func (h *subjectRegistryTestHandler) Decide(_ context.Context, _ DecisionCommand) (DecisionResult, error) {
	h.decided = true
	return DecisionResult{Instance: approval.Instance{SubjectType: h.subjectType}}, nil
}
func (h *subjectRegistryTestHandler) Cancel(_ context.Context, _ CancelCommand) (DecisionResult, error) {
	h.cancelled = true
	return DecisionResult{Instance: approval.Instance{SubjectType: h.subjectType}}, nil
}
func (h *subjectRegistryTestHandler) PresentPending(context.Context, SubmitResult) (app.AppNotificationResult, error) {
	h.presented = true
	if h.presentErr != nil {
		return app.AppNotificationResult{}, h.presentErr
	}
	return app.AppNotificationResult{NotificationID: "pending"}, nil
}
func (h *subjectRegistryTestHandler) PresentResolved(context.Context, DecisionResult, PresentationContext) (app.AppNotificationResult, error) {
	h.resolved = true
	if h.presentErr != nil {
		return app.AppNotificationResult{}, h.presentErr
	}
	return app.AppNotificationResult{NotificationID: "resolved"}, nil
}

func TestSubjectRegistryDispatchesTransportNeutralLifecycle(t *testing.T) {
	h := &subjectRegistryTestHandler{subjectType: "custom"}
	instance := approval.Instance{
		ID: "appr_custom", WorkspaceID: "wsp_custom",
		SubjectType: "custom", Version: 2,
	}
	marks := map[string]int64{}
	registry := NewSubjectRegistry(subjectRegistryTestLoader{
		instance: instance, marks: marks,
	})
	require.NoError(t, registry.Register(h))
	registry.Seal()
	ctx := context.Background()
	submitted, err := registry.SubmitRequest(ctx, "custom", SubmitCommand{})
	require.NoError(t, err)
	submitted.Instance = instance
	_, err = registry.Decide(ctx, DecisionCommand{
		WorkspaceID: instance.WorkspaceID,
		ApprovalID:  instance.ID,
	})
	require.NoError(t, err)
	cancelled, err := registry.Cancel(ctx, CancelCommand{
		Principal:  SubjectPrincipal{WorkspaceID: instance.WorkspaceID},
		ApprovalID: instance.ID,
	})
	require.NoError(t, err)
	cancelled.Instance = instance
	_, err = registry.PresentPending(ctx, submitted)
	require.NoError(t, err)
	_, err = registry.PresentResolved(ctx, cancelled, PresentationContext{})
	require.NoError(t, err)
	require.True(t, h.validated)
	require.True(t, h.submitted)
	require.True(t, h.decided)
	require.True(t, h.cancelled)
	require.True(t, h.presented)
	require.True(t, h.resolved)
	require.Equal(t, instance.Version, marks[instance.ID])
	require.Equal(t, []string{"custom"}, registry.ReconcileTypes())
}
func TestSubjectRegistryFailsClosed(t *testing.T) {
	registry := NewSubjectRegistry()
	_, err := registry.SubmitRequest(context.Background(), "unknown", SubmitCommand{})
	require.ErrorContains(t, err, "not sealed")
	registry.Seal()
	_, err = registry.SubmitRequest(context.Background(), "unknown", SubmitCommand{})
	require.ErrorContains(t, err, "not registered")
}
func TestSubjectRegistryRejectsDuplicateTypes(t *testing.T) {
	registry := NewSubjectRegistry()
	require.NoError(t, registry.Register(&subjectRegistryTestHandler{subjectType: "custom"}))
	require.Error(t, registry.Register(&subjectRegistryTestHandler{subjectType: "custom"}))
	require.Error(t, registry.Register(&subjectRegistryTestHandler{
		subjectType: "other", requestType: "custom",
	}))
}

func TestSubjectRegistryEnforcesCancelPolicy(t *testing.T) {
	for _, policy := range []SubjectCancelPolicy{
		SubjectCancelNotSupported,
		SubjectCancelPolicy("applicnat"),
	} {
		t.Run(string(policy), func(t *testing.T) {
			instance := approval.Instance{
				ID: "appr_no_cancel", WorkspaceID: "wsp_no_cancel",
				SubjectType: "no_cancel",
			}
			handler := &subjectRegistryTestHandler{
				subjectType:  "no_cancel",
				cancelPolicy: policy,
			}
			registry := NewSubjectRegistry(subjectRegistryTestLoader{
				instance: instance, marks: map[string]int64{},
			})
			require.NoError(t, registry.Register(handler))
			registry.Seal()

			_, err := registry.Cancel(
				context.Background(),
				CancelCommand{
					Principal:  SubjectPrincipal{WorkspaceID: instance.WorkspaceID},
					ApprovalID: instance.ID,
				},
			)
			require.ErrorContains(t, err, "does not support cancellation")
			require.True(t, handler.validated)
			require.False(t, handler.cancelled)
		})
	}
}

func TestSubjectRegistryLoadsAuthoritativeInstanceForDispatch(t *testing.T) {
	instance := approval.Instance{
		ID: "appr_authoritative", WorkspaceID: "wsp_authoritative",
		SubjectType: "authoritative",
	}
	handler := &subjectRegistryTestHandler{subjectType: instance.SubjectType}
	registry := NewSubjectRegistry(subjectRegistryTestLoader{
		instance: instance, marks: map[string]int64{},
	})
	require.NoError(t, registry.Register(handler))
	registry.Seal()

	_, err := registry.Decide(context.Background(), DecisionCommand{
		WorkspaceID: "wsp_other",
		ApprovalID:  instance.ID,
	})
	require.Error(t, err)
	require.False(t, handler.validated)
	require.False(t, handler.decided)

	_, err = registry.Decide(context.Background(), DecisionCommand{
		WorkspaceID: instance.WorkspaceID,
		ApprovalID:  "appr_other",
	})
	require.Error(t, err)
	require.False(t, handler.decided)
}

func TestSubjectRegistryDoesNotMarkFailedPresentation(t *testing.T) {
	instance := approval.Instance{
		ID: "appr_present_failure", WorkspaceID: "wsp_present_failure",
		SubjectType: "present_failure", Version: 1,
	}
	marks := map[string]int64{}
	handler := &subjectRegistryTestHandler{
		subjectType: instance.SubjectType,
		presentErr:  fmt.Errorf("projection failed"),
	}
	registry := NewSubjectRegistry(subjectRegistryTestLoader{
		instance: instance, marks: marks,
	})
	require.NoError(t, registry.Register(handler))
	registry.Seal()

	_, err := registry.PresentPending(context.Background(), SubmitResult{
		Instance: instance,
	})
	require.ErrorContains(t, err, "projection failed")
	require.Empty(t, marks)

	_, err = registry.PresentResolved(context.Background(), DecisionResult{
		Instance: instance,
	}, PresentationContext{})
	require.ErrorContains(t, err, "projection failed")
	require.Empty(t, marks)
}
