package prmap

import (
	"errors"
	"testing"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	pkgmodel "github.com/tech/sendico/pkg/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestMap_Success(t *testing.T) {
	mapper := New()
	payment := newPaymentFixture()

	out, err := mapper.Map(MapInput{Payment: payment})
	if err != nil {
		t.Fatalf("Map returned error: %v", err)
	}
	if out == nil || out.Payment == nil {
		t.Fatalf("expected mapped payment")
	}

	protoPayment := out.Payment
	if got, want := protoPayment.GetPaymentRef(), payment.PaymentRef; got != want {
		t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := protoPayment.GetState(), orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_EXECUTING; got != want {
		t.Fatalf("state mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := protoPayment.GetCreatedAt().AsTime().UTC(), payment.CreatedAt.UTC(); !got.Equal(want) {
		t.Fatalf("created_at mismatch: got=%v want=%v", got, want)
	}
	if got, want := protoPayment.GetUpdatedAt().AsTime().UTC(), payment.UpdatedAt.UTC(); !got.Equal(want) {
		t.Fatalf("updated_at mismatch: got=%v want=%v", got, want)
	}

	intent := protoPayment.GetIntentSnapshot()
	if intent == nil {
		t.Fatalf("expected intent_snapshot")
	}
	if got, want := intent.GetSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
		t.Fatalf("settlement_mode mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := intent.GetFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION; got != want {
		t.Fatalf("fee_treatment mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := intent.GetComment(), "invoice-7"; got != want {
		t.Fatalf("comment mismatch: got=%q want=%q", got, want)
	}
	if source := intent.GetSource().GetPaymentMethod(); source == nil {
		t.Fatalf("expected source payment_method")
	} else {
		if got, want := source.GetType(), endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET; got != want {
			t.Fatalf("source method type mismatch: got=%s want=%s", got.String(), want.String())
		}
		var wallet pkgmodel.WalletPaymentData
		if err := bson.Unmarshal(source.GetData(), &wallet); err != nil {
			t.Fatalf("failed to decode wallet data: %v", err)
		}
		if got, want := wallet.WalletID, "mw-src"; got != want {
			t.Fatalf("wallet id mismatch: got=%q want=%q", got, want)
		}
	}
	if destination := intent.GetDestination().GetPaymentMethod(); destination == nil {
		t.Fatalf("expected destination payment_method")
	} else if got, want := destination.GetType(), endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN; got != want {
		t.Fatalf("destination method type mismatch: got=%s want=%s", got.String(), want.String())
	}

	quote := protoPayment.GetQuoteSnapshot()
	if quote == nil {
		t.Fatalf("expected quote_snapshot")
	}
	if got, want := quote.GetQuoteRef(), payment.QuotationRef; got != want {
		t.Fatalf("quote_ref mismatch: got=%q want=%q", got, want)
	}
	if got, want := quote.GetState(), quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE; got != want {
		t.Fatalf("quote state mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED; got != want {
		t.Fatalf("resolved_settlement_mode mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
		t.Fatalf("resolved_fee_treatment mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := quote.GetIntentRef(), payment.IntentSnapshot.Ref; got != want {
		t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
	}

	steps := protoPayment.GetStepExecutions()
	if len(steps) != 2 {
		t.Fatalf("step count mismatch: got=%d want=2", len(steps))
	}
	if got, want := steps[0].GetAttempt(), uint32(1); got != want {
		t.Fatalf("attempt normalization mismatch: got=%d want=%d", got, want)
	}
	if got, want := steps[1].GetFailure().GetCategory(), sharedv1.PaymentFailureCode_FAILURE_BALANCE; got != want {
		t.Fatalf("failure category mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := steps[1].GetRefs()[0].GetRail(), gatewayv1.Rail_RAIL_LEDGER; got != want {
		t.Fatalf("external ref rail mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := steps[0].GetReportVisibility(), orchestrationv2.ReportVisibility_REPORT_VISIBILITY_USER; got != want {
		t.Fatalf("report_visibility mismatch: got=%s want=%s", got.String(), want.String())
	}
	if got, want := steps[0].GetUserLabel(), "Card payout"; got != want {
		t.Fatalf("user_label mismatch: got=%q want=%q", got, want)
	}
	if steps[0].GetMoney() == nil {
		t.Fatal("expected structured money envelope to be mapped")
	}
	if steps[0].GetMoney().GetPlanned() == nil {
		t.Fatal("expected structured planned money snapshot")
	}
	if got, want := steps[0].GetMoney().GetPlanned().GetAmount().GetAmount(), "96"; got != want {
		t.Fatalf("money.planned.amount.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := steps[0].GetMoney().GetPlanned().GetConvertedAmount().GetAmount(), "91"; got != want {
		t.Fatalf("money.planned.converted_amount.amount mismatch: got=%q want=%q", got, want)
	}
	if steps[0].GetMoney().GetExecuted() == nil {
		t.Fatal("expected structured executed money snapshot")
	}
	if got, want := steps[0].GetMoney().GetExecuted().GetAmount().GetAmount(), "95"; got != want {
		t.Fatalf("money.executed.amount.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := steps[0].GetMoney().GetExecuted().GetConvertedAmount().GetAmount(), "90"; got != want {
		t.Fatalf("money.executed.converted_amount.amount mismatch: got=%q want=%q", got, want)
	}
	if got, want := steps[1].GetReportVisibility(), orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN; got != want {
		t.Fatalf("report_visibility mismatch: got=%s want=%s", got.String(), want.String())
	}
}

func TestMap_InvalidArguments(t *testing.T) {
	mapper := New()
	now := time.Date(2026, time.January, 11, 12, 0, 0, 0, time.UTC)

	tests := []struct {
		name    string
		payment *agg.Payment
	}{
		{
			name: "nil payment",
		},
		{
			name: "missing payment ref",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.PaymentRef = ""
				return p
			}(),
		},
		{
			name: "missing quote snapshot",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.QuoteSnapshot = nil
				return p
			}(),
		},
		{
			name: "invalid aggregate state",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.State = agg.StateUnspecified
				return p
			}(),
		},
		{
			name: "invalid step state",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.StepExecutions[0].State = agg.StepStateUnspecified
				return p
			}(),
		},
		{
			name: "missing intent amount",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.IntentSnapshot.Amount = nil
				return p
			}(),
		},
		{
			name: "unsupported endpoint type",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.IntentSnapshot.Source.Type = model.EndpointTypeUnspecified
				return p
			}(),
		},
		{
			name: "missing timestamps",
			payment: func() *agg.Payment {
				p := newPaymentFixture()
				p.CreatedAt = now
				p.UpdatedAt = time.Time{}
				return p
			}(),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := mapper.Map(MapInput{Payment: tt.payment})
			if !errors.Is(err, merrors.ErrInvalidArg) {
				t.Fatalf("expected invalid argument error, got %v", err)
			}
		})
	}
}

func newPaymentFixture() *agg.Payment {
	createdAt := time.Date(2026, time.January, 11, 12, 0, 0, 0, time.UTC)
	startedAt := createdAt.Add(2 * time.Minute)

	return &agg.Payment{
		Base: storable.Base{
			ID:        bson.NewObjectID(),
			CreatedAt: createdAt,
			UpdatedAt: createdAt.Add(5 * time.Minute),
		},
		OrganizationBoundBase: pkgmodel.OrganizationBoundBase{
			OrganizationRef: bson.NewObjectID(),
		},
		PaymentRef:       "pay-1",
		IdempotencyKey:   "idem-1",
		QuotationRef:     "quote-1",
		ClientPaymentRef: "client-1",
		IntentSnapshot: model.PaymentIntent{
			Ref: "intent-1",
			Source: model.PaymentEndpoint{
				Type: model.EndpointTypeManagedWallet,
				ManagedWallet: &model.ManagedWalletEndpoint{
					ManagedWalletRef: "mw-src",
					Asset: &paymenttypes.Asset{
						Chain:       "TRON",
						TokenSymbol: "USDT",
					},
				},
			},
			Destination: model.PaymentEndpoint{
				Type: model.EndpointTypeCard,
				Card: &model.CardEndpoint{
					Token:      "tok_1",
					MaskedPan:  "1234",
					Cardholder: "John",
					ExpMonth:   12,
					ExpYear:    2030,
					Country:    "US",
				},
			},
			Amount: &paymenttypes.Money{
				Amount:   "100",
				Currency: "USDT",
			},
			SettlementMode:     model.SettlementModeFixSource,
			FeeTreatment:       model.FeeTreatmentDeductFromDestination,
			SettlementCurrency: "USD",
			Comment:            "invoice-7",
		},
		QuoteSnapshot: &model.PaymentQuoteSnapshot{
			DebitAmount: &paymenttypes.Money{
				Amount:   "100",
				Currency: "USDT",
			},
			ExpectedSettlementAmount: &paymenttypes.Money{
				Amount:   "95",
				Currency: "USD",
			},
			TotalCost: &paymenttypes.Money{
				Amount:   "101",
				Currency: "USDT",
			},
			FeeLines: []*paymenttypes.FeeLine{
				{
					LedgerAccountRef: "fees:1",
					Money: &paymenttypes.Money{
						Amount:   "1",
						Currency: "USDT",
					},
					LineType: paymenttypes.PostingLineTypeFee,
					Side:     paymenttypes.EntrySideDebit,
					Meta:     map[string]string{"bucket": "service"},
				},
			},
			FeeRules: []*paymenttypes.AppliedRule{
				{
					RuleID:      "rule-1",
					RuleVersion: "v1",
					Formula:     "x*0.01",
					Rounding:    paymenttypes.RoundingModeHalfUp,
					TaxCode:     "VAT",
					TaxRate:     "0.10",
					Parameters:  map[string]string{"jurisdiction": "EU"},
				},
			},
			FXQuote: &paymenttypes.FXQuote{
				QuoteRef: "fx-1",
				Pair: &paymenttypes.CurrencyPair{
					Base:  "USDT",
					Quote: "USD",
				},
				Side: paymenttypes.FXSideSellBaseBuyQuote,
				Price: &paymenttypes.Decimal{
					Value: "0.95",
				},
				BaseAmount: &paymenttypes.Money{
					Amount:   "100",
					Currency: "USDT",
				},
				QuoteAmount: &paymenttypes.Money{
					Amount:   "95",
					Currency: "USD",
				},
				ExpiresAtUnixMs: createdAt.Add(10 * time.Minute).UnixMilli(),
				PricedAtUnixMs:  createdAt.Add(-1 * time.Minute).UnixMilli(),
				Provider:        "oracle-1",
				RateRef:         "rate-1",
				Firm:            true,
			},
			Route: &paymenttypes.QuoteRouteSpecification{
				Rail:              "CARD",
				Provider:          "provider-1",
				PayoutMethod:      "CARD",
				Network:           "VISA",
				RouteRef:          "route-1",
				PricingProfileRef: "pricing-1",
				Settlement: &paymenttypes.QuoteRouteSettlement{
					Model: "FIX_RECEIVED",
					Asset: &paymenttypes.Asset{
						Chain:       "TRON",
						TokenSymbol: "USDT",
					},
				},
				Hops: []*paymenttypes.QuoteRouteHop{
					{
						Index: 10,
						Rail:  "LEDGER",
						Role:  paymenttypes.QuoteRouteHopRoleSource,
					},
					{
						Index:      20,
						Rail:       "CARD",
						Gateway:    "gw-card",
						InstanceID: "card-1",
						Role:       paymenttypes.QuoteRouteHopRoleDestination,
					},
				},
			},
			ExecutionConditions: &paymenttypes.QuoteExecutionConditions{
				Readiness:                         paymenttypes.QuoteExecutionReadinessLiquidityReady,
				BatchingEligible:                  true,
				PrefundingRequired:                false,
				PrefundingCostIncluded:            true,
				LiquidityCheckRequiredAtExecution: true,
				LatencyHint:                       "fast",
				Assumptions:                       []string{"funds_ready"},
			},
		},
		State:   agg.StateExecuting,
		Version: 3,
		StepExecutions: []agg.StepExecution{
			{
				StepRef:          "s1",
				StepCode:         "hop.20.card_payout.send",
				ReportVisibility: model.ReportVisibilityUser,
				UserLabel:        " Card payout ",
				State:            agg.StepStateRunning,
				Attempt:          0,
				ExecutedMoney: &paymenttypes.Money{
					Amount:   "95",
					Currency: "USD",
				},
				ConvertedMoney: &paymenttypes.Money{
					Amount:   "90",
					Currency: "EUR",
				},
				PlannedMoney: &paymenttypes.Money{
					Amount:   "96",
					Currency: "USD",
				},
				PlannedConvertedMoney: &paymenttypes.Money{
					Amount:   "91",
					Currency: "EUR",
				},
				StartedAt: &startedAt,
			},
			{
				StepRef:          "s2",
				StepCode:         "edge.10_20.ledger.debit",
				ReportVisibility: model.ReportVisibilityHidden,
				State:            agg.StepStateFailed,
				Attempt:          2,
				FailureCode:      "ledger_balance_low",
				FailureMsg:       "insufficient balance",
				ExternalRefs: []agg.ExternalRef{
					{
						GatewayInstanceID: "ledger-1",
						Kind:              "ledger_entry_ref",
						Ref:               "entry-1",
					},
				},
			},
		},
	}
}
