package agg

import (
	"errors"
	"testing"
	"time"

	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func TestCreate_OK(t *testing.T) {
	now := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC)
	orgID := bson.NewObjectID()
	paymentID := bson.NewObjectID()

	factory := &svc{
		logger: zap.NewNop(),
		now:    func() time.Time { return now },
		newID: func() bson.ObjectID {
			return paymentID
		},
	}

	intent := model.PaymentIntent{
		Ref:  "intent-1",
		Kind: model.PaymentKindPayout,
	}
	quote := &model.PaymentQuoteSnapshot{
		QuoteRef: "",
	}

	payment, err := factory.Create(Input{
		OrganizationRef:  orgID,
		IdempotencyKey:   " idem-1 ",
		QuotationRef:     " quote-1 ",
		ClientPaymentRef: " client-1 ",
		IntentSnapshot:   intent,
		QuoteSnapshot:    quote,
		Steps: []StepShell{
			{StepRef: " s1 ", StepCode: " reserve_funds ", ReportVisibility: model.ReportVisibilityHidden},
			{
				StepRef:          "s2",
				StepCode:         "submit_gateway",
				Rail:             discovery.RailProviderSettlement,
				Gateway:          "payment_gateway_settlement",
				InstanceID:       "04a54fec-20f4-4250-a715-eb9886e13e12",
				ReportVisibility: model.ReportVisibilityUser,
				UserLabel:        " Card payout ",
			},
		},
	})
	if err != nil {
		t.Fatalf("Create returned error: %v", err)
	}
	if payment == nil {
		t.Fatal("expected aggregate")
	}

	if got, want := payment.ID, paymentID; got != want {
		t.Fatalf("id mismatch: got=%s want=%s", got.Hex(), want.Hex())
	}
	if got, want := payment.PaymentRef, paymentID.Hex(); got != want {
		t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payment.OrganizationRef, orgID; got != want {
		t.Fatalf("organization mismatch: got=%s want=%s", got.Hex(), want.Hex())
	}
	if got, want := payment.IdempotencyKey, "idem-1"; got != want {
		t.Fatalf("idempotency_key mismatch: got=%q want=%q", got, want)
	}
	if got, want := payment.QuotationRef, "quote-1"; got != want {
		t.Fatalf("quotation_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payment.ClientPaymentRef, "client-1"; got != want {
		t.Fatalf("client_payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := payment.State, StateCreated; got != want {
		t.Fatalf("state mismatch: got=%q want=%q", got, want)
	}
	if got, want := payment.Version, initialVersion; got != want {
		t.Fatalf("version mismatch: got=%d want=%d", got, want)
	}
	if got, want := payment.CreatedAt, now; got != want {
		t.Fatalf("created_at mismatch: got=%v want=%v", got, want)
	}
	if got, want := payment.UpdatedAt, now; got != want {
		t.Fatalf("updated_at mismatch: got=%v want=%v", got, want)
	}

	if got, want := payment.IntentSnapshot.Ref, "intent-1"; got != want {
		t.Fatalf("intent_snapshot.ref mismatch: got=%q want=%q", got, want)
	}
	if payment.QuoteSnapshot == nil {
		t.Fatal("expected quote_snapshot")
	}
	if got, want := payment.QuoteSnapshot.QuoteRef, "quote-1"; got != want {
		t.Fatalf("quote_snapshot.quote_ref mismatch: got=%q want=%q", got, want)
	}

	if len(payment.StepExecutions) != 2 {
		t.Fatalf("expected 2 step executions, got %d", len(payment.StepExecutions))
	}
	if payment.StepExecutions[0].StepRef != "s1" || payment.StepExecutions[0].StepCode != "reserve_funds" {
		t.Fatalf("unexpected first step: %+v", payment.StepExecutions[0])
	}
	if payment.StepExecutions[0].State != StepStatePending || payment.StepExecutions[0].Attempt != 1 {
		t.Fatalf("unexpected first step shell state: %+v", payment.StepExecutions[0])
	}
	if got, want := payment.StepExecutions[0].ReportVisibility, model.ReportVisibilityHidden; got != want {
		t.Fatalf("unexpected first step visibility: got=%q want=%q", got, want)
	}
	if got, want := payment.StepExecutions[1].ReportVisibility, model.ReportVisibilityUser; got != want {
		t.Fatalf("unexpected second step visibility: got=%q want=%q", got, want)
	}
	if got, want := payment.StepExecutions[1].UserLabel, "Card payout"; got != want {
		t.Fatalf("unexpected second step user label: got=%q want=%q", got, want)
	}
	if got, want := payment.StepExecutions[1].Rail, model.Rail(discovery.RailProviderSettlement); got != want {
		t.Fatalf("unexpected second step rail: got=%q want=%q", got, want)
	}
	if got, want := payment.StepExecutions[1].Gateway, "payment_gateway_settlement"; got != want {
		t.Fatalf("unexpected second step gateway: got=%q want=%q", got, want)
	}
	if got, want := payment.StepExecutions[1].InstanceID, "04a54fec-20f4-4250-a715-eb9886e13e12"; got != want {
		t.Fatalf("unexpected second step instance_id: got=%q want=%q", got, want)
	}

	// Verify immutable snapshot semantics by ensuring clones were created.
	payment.IntentSnapshot.Ref = "changed"
	payment.QuoteSnapshot.QuoteRef = "changed"
	if intent.Ref != "intent-1" {
		t.Fatalf("expected original intent unchanged, got %q", intent.Ref)
	}
	if quote.QuoteRef != "" {
		t.Fatalf("expected original quote unchanged, got %q", quote.QuoteRef)
	}
}

func TestCreate_QuoteRefMismatch(t *testing.T) {
	factory := New()

	_, err := factory.Create(Input{
		OrganizationRef: bson.NewObjectID(),
		IdempotencyKey:  "idem-1",
		QuotationRef:    "quote-1",
		IntentSnapshot: model.PaymentIntent{
			Kind:   model.PaymentKindPayout,
			Amount: testMoney(),
		},
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			QuoteRef: "quote-2",
		},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument, got %v", err)
	}
}

func TestCreate_NoStepsProducesEmptyShell(t *testing.T) {
	factory := New()

	payment, err := factory.Create(Input{
		OrganizationRef: bson.NewObjectID(),
		IdempotencyKey:  "idem-1",
		QuotationRef:    "quote-1",
		IntentSnapshot: model.PaymentIntent{
			Kind:   model.PaymentKindPayout,
			Amount: testMoney(),
		},
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			QuoteRef: "quote-1",
		},
	})
	if err != nil {
		t.Fatalf("Create returned error: %v", err)
	}
	if len(payment.StepExecutions) != 0 {
		t.Fatalf("expected empty step telemetry shell, got %d", len(payment.StepExecutions))
	}
}

func TestCreate_InputValidation(t *testing.T) {
	tests := []struct {
		name string
		in   Input
	}{
		{
			name: "missing organization_id",
			in: Input{
				IdempotencyKey: "idem-1",
				QuotationRef:   "quote-1",
				IntentSnapshot: model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:  &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
			},
		},
		{
			name: "missing idempotency_key",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				QuotationRef:    "quote-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
			},
		},
		{
			name: "missing quotation_ref",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
			},
		},
		{
			name: "missing intent_snapshot",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				QuotationRef:    "quote-1",
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
			},
		},
		{
			name: "missing quote_snapshot",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				QuotationRef:    "quote-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
			},
		},
		{
			name: "step missing ref",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				QuotationRef:    "quote-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
				Steps: []StepShell{
					{StepRef: " ", StepCode: "code"},
				},
			},
		},
		{
			name: "step missing code",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				QuotationRef:    "quote-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
				Steps: []StepShell{
					{StepRef: "s1", StepCode: " "},
				},
			},
		},
		{
			name: "step report visibility invalid",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				QuotationRef:    "quote-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
				Steps: []StepShell{
					{StepRef: "s1", StepCode: "code-1", ReportVisibility: model.ReportVisibility("invalid")},
				},
			},
		},
		{
			name: "step ref must be unique",
			in: Input{
				OrganizationRef: bson.NewObjectID(),
				IdempotencyKey:  "idem-1",
				QuotationRef:    "quote-1",
				IntentSnapshot:  model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
				QuoteSnapshot:   &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
				Steps: []StepShell{
					{StepRef: "s1", StepCode: "code-1"},
					{StepRef: "s1", StepCode: "code-2"},
				},
			},
		},
	}

	factory := New()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := factory.Create(tt.in)
			if err == nil {
				t.Fatal("expected error")
			}
		})
	}
}

func testMoney() *modelMoney {
	return &modelMoney{Amount: "10", Currency: "USD"}
}

// modelMoney is a minimal compatibility shim for tests without depending on payments/types constructors.
type modelMoney = paymenttypes.Money
