package quote_response_mapper_v2

import (
	"errors"
	"testing"
	"time"

	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
)

func TestMap_ExecutableQuote(t *testing.T) {
	mapper := New()
	createdAt := time.Unix(100, 0)
	updatedAt := time.Unix(120, 0)
	expiresAt := time.Unix(200, 0)
	pricedAt := time.Unix(150, 0)

	out, err := mapper.Map(MapInput{
		Meta: QuoteMeta{
			ID:        "rec-1",
			CreatedAt: createdAt,
			UpdatedAt: updatedAt,
		},
		Quote: CanonicalQuote{
			QuoteRef: "q-1",
			TransferPrincipalAmount: &moneyv1.Money{
				Amount:   "10",
				Currency: "USD",
			},
			DestinationAmount: &moneyv1.Money{
				Amount:   "9",
				Currency: "EUR",
			},
			PayerTotalDebitAmount: &moneyv1.Money{
				Amount:   "10.2",
				Currency: "USD",
			},
			Route: &quotationv2.RouteSpecification{
				Rail:         "CARD",
				Provider:     paymenttypes.DefaultCardsGatewayID,
				PayoutMethod: "CARD",
				Settlement: &quotationv2.RouteSettlement{
					Asset: &paymentv1.ChainAsset{
						Key: &paymentv1.ChainAssetKey{
							TokenSymbol: "USD",
						},
					},
					Model: "FIX_SOURCE",
				},
			},
			Conditions: &quotationv2.ExecutionConditions{
				Readiness:                         quotationv2.QuoteExecutionReadiness_QUOTE_EXECUTION_READINESS_LIQUIDITY_READY,
				BatchingEligible:                  true,
				PrefundingRequired:                false,
				LiquidityCheckRequiredAtExecution: true,
			},
			ResolvedSettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE,
			ResolvedFeeTreatment:   quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
			ExpiresAt:              expiresAt,
			PricedAt:               pricedAt,
		},
		Status: QuoteStatus{
			State: quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out == nil || out.Quote == nil {
		t.Fatalf("expected mapped quote")
	}
	if got, want := out.State, quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE; got != want {
		t.Fatalf("unexpected state: got=%s want=%s", got.String(), want.String())
	}
	if got := out.BlockReason; got != quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_UNSPECIFIED {
		t.Fatalf("expected empty block reason, got=%s", got.String())
	}
	if got, want := out.Quote.GetState(), quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE; got != want {
		t.Fatalf("unexpected proto state: got=%s want=%s", got.String(), want.String())
	}
	if out.Quote.GetBlockReason() != quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_UNSPECIFIED {
		t.Fatalf("expected empty proto block reason")
	}
	if out.Quote.GetStorable().GetId() != "rec-1" {
		t.Fatalf("expected storable id rec-1, got %q", out.Quote.GetStorable().GetId())
	}
	if got := out.Quote.GetStorable().GetCreatedAt().AsTime(); !got.Equal(createdAt) {
		t.Fatalf("unexpected created_at: %v", got)
	}
	if got := out.Quote.GetStorable().GetUpdatedAt().AsTime(); !got.Equal(updatedAt) {
		t.Fatalf("unexpected updated_at: %v", got)
	}
	if got := out.Quote.GetExpiresAt().AsTime(); !got.Equal(expiresAt) {
		t.Fatalf("unexpected expires_at: %v", got)
	}
	if got := out.Quote.GetPricedAt().AsTime(); !got.Equal(pricedAt) {
		t.Fatalf("unexpected priced_at: %v", got)
	}
	if got, want := out.Quote.GetRoute().GetProvider(), paymenttypes.DefaultCardsGatewayID; got != want {
		t.Fatalf("unexpected route provider: got=%q want=%q", got, want)
	}
	if got, want := out.Quote.GetRoute().GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
		t.Fatalf("unexpected settlement token: got=%q want=%q", got, want)
	}
	if got, want := out.Quote.GetPayerTotalDebitAmount().GetAmount(), "10.2"; got != want {
		t.Fatalf("unexpected payer_total_debit_amount: got=%q want=%q", got, want)
	}
	if got, want := out.Quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
		t.Fatalf("unexpected resolved_settlement_mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := out.Quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
		t.Fatalf("unexpected resolved_fee_treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestMap_ClampsQuoteExpiresAtToFXExpiry(t *testing.T) {
	mapper := New()
	quoteExpiresAt := time.Unix(300, 0).UTC()
	fxExpiresAt := time.Unix(200, 0).UTC()

	out, err := mapper.Map(MapInput{
		Quote: CanonicalQuote{
			QuoteRef:  "q-fx-expiry-clamp",
			ExpiresAt: quoteExpiresAt,
			FXQuote:   &oraclev1.Quote{ExpiresAtUnixMs: fxExpiresAt.UnixMilli()},
			TransferPrincipalAmount: &moneyv1.Money{
				Amount:   "1",
				Currency: "USDT",
			},
		},
		Status: QuoteStatus{
			State: quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out == nil || out.Quote == nil || out.Quote.GetExpiresAt() == nil {
		t.Fatalf("expected mapped quote with expires_at")
	}
	if got, want := out.Quote.GetExpiresAt().AsTime().UnixMilli(), fxExpiresAt.UnixMilli(); got != want {
		t.Fatalf("unexpected clamped expires_at: got=%d want=%d", got, want)
	}
}

func TestMap_DefaultsResolvedEconomicsWhenUnset(t *testing.T) {
	mapper := New()
	out, err := mapper.Map(MapInput{
		Quote: CanonicalQuote{
			TransferPrincipalAmount: &moneyv1.Money{Amount: "1", Currency: "USDT"},
		},
		Status: QuoteStatus{
			State: quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out == nil || out.Quote == nil {
		t.Fatalf("expected mapped quote")
	}
	if got, want := out.Quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE; got != want {
		t.Fatalf("unexpected default resolved_settlement_mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := out.Quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
		t.Fatalf("unexpected default resolved_fee_treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestMap_PreservesIndependentResolvedFeeTreatment(t *testing.T) {
	mapper := New()
	out, err := mapper.Map(MapInput{
		Quote: CanonicalQuote{
			TransferPrincipalAmount: &moneyv1.Money{Amount: "1", Currency: "USDT"},
			ResolvedSettlementMode:  paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED,
			ResolvedFeeTreatment:    quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE,
		},
		Status: QuoteStatus{
			State: quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out == nil || out.Quote == nil {
		t.Fatalf("expected mapped quote")
	}
	if got, want := out.Quote.GetResolvedSettlementMode(), paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED; got != want {
		t.Fatalf("unexpected resolved_settlement_mode: got=%s want=%s", got.String(), want.String())
	}
	if got, want := out.Quote.GetResolvedFeeTreatment(), quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE; got != want {
		t.Fatalf("unexpected resolved_fee_treatment: got=%s want=%s", got.String(), want.String())
	}
}

func TestMap_BlockedQuote(t *testing.T) {
	mapper := New()
	out, err := mapper.Map(MapInput{
		Quote: CanonicalQuote{
			QuoteRef: "q-2",
		},
		Status: QuoteStatus{
			State:       quotationv2.QuoteState_QUOTE_STATE_BLOCKED,
			BlockReason: quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE,
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out == nil || out.Quote == nil {
		t.Fatalf("expected mapped quote")
	}
	if got, want := out.Quote.GetState(), quotationv2.QuoteState_QUOTE_STATE_BLOCKED; got != want {
		t.Fatalf("unexpected state: got=%s want=%s", got.String(), want.String())
	}
	if got := out.Quote.GetBlockReason(); got != quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE {
		t.Fatalf("unexpected block reason: %s", got.String())
	}
}

func TestMap_IndicativeAndExpiredAreValidWithoutBlockReason(t *testing.T) {
	mapper := New()
	states := []quotationv2.QuoteState{
		quotationv2.QuoteState_QUOTE_STATE_INDICATIVE,
		quotationv2.QuoteState_QUOTE_STATE_EXPIRED,
	}
	for _, state := range states {
		out, err := mapper.Map(MapInput{
			Status: QuoteStatus{
				State: state,
			},
		})
		if err != nil {
			t.Fatalf("unexpected error for state=%s: %v", state.String(), err)
		}
		if out == nil || out.Quote == nil {
			t.Fatalf("expected mapped quote for state=%s", state.String())
		}
		if got := out.Quote.GetBlockReason(); got != quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_UNSPECIFIED {
			t.Fatalf("expected empty block reason for state=%s, got=%s", state.String(), got.String())
		}
	}
}

func TestMap_StateInvariants(t *testing.T) {
	mapper := New()

	_, err := mapper.Map(MapInput{
		Status: QuoteStatus{
			State: quotationv2.QuoteState_QUOTE_STATE_UNSPECIFIED,
		},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid arg for unspecified state, got %v", err)
	}

	_, err = mapper.Map(MapInput{
		Status: QuoteStatus{
			State: quotationv2.QuoteState_QUOTE_STATE_BLOCKED,
		},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid arg for blocked without reason, got %v", err)
	}

	_, err = mapper.Map(MapInput{
		Status: QuoteStatus{
			State:       quotationv2.QuoteState_QUOTE_STATE_EXECUTABLE,
			BlockReason: quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_LIMIT_BLOCKED,
		},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid arg for non-blocked with block reason, got %v", err)
	}
}
