package quotation

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/tech/sendico/pkg/merrors"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/proto"
)

func TestBuildPaymentQuote_RequestsConversionFeesWithoutPlanTemplates(t *testing.T) {
	feeClient := &stubFeeEngineClient{
		precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
			"payments.orchestrator.quote": {
				Lines: []*feesv1.DerivedPostingLine{
					testFeeLine("1.00", "USDT"),
				},
				Applied: []*feesv1.AppliedRule{{RuleId: "rule.base"}},
			},
			"payments.orchestrator.conversion_quote": {
				Lines: []*feesv1.DerivedPostingLine{
					testFeeLine("2.00", "USDT"),
				},
				Applied: []*feesv1.AppliedRule{{RuleId: "rule.conversion"}},
			},
		},
	}

	svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
	req := &quoteRequest{
		Meta:           &sharedv1.RequestMeta{OrganizationRef: "org_1"},
		IdempotencyKey: "idem_1",
		Intent:         testManagedWalletToCardIntent(),
	}

	quote, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
	if err != nil {
		t.Fatalf("buildPaymentQuote returned error: %v", err)
	}
	if quote == nil {
		t.Fatalf("expected quote")
	}

	if got, want := len(feeClient.precomputeReqs), 2; got != want {
		t.Fatalf("unexpected precompute call count: got=%d want=%d", got, want)
	}
	if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.quote"), 1; got != want {
		t.Fatalf("unexpected base precompute calls: got=%d want=%d", got, want)
	}
	if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.conversion_quote"), 1; got != want {
		t.Fatalf("unexpected conversion precompute calls: got=%d want=%d", got, want)
	}
	if got, want := len(quote.GetFeeLines()), 2; got != want {
		t.Fatalf("unexpected fee lines count: got=%d want=%d", got, want)
	}
	if quote.GetExpectedFeeTotal() == nil {
		t.Fatalf("expected fee total")
	}
	if got, want := quote.GetExpectedFeeTotal().GetCurrency(), "USDT"; got != want {
		t.Fatalf("unexpected fee total currency: got=%q want=%q", got, want)
	}
	if got, want := quote.GetExpectedFeeTotal().GetAmount(), "3"; got != want {
		t.Fatalf("unexpected fee total amount: got=%q want=%q", got, want)
	}
	if got := quote.GetFeeLines()[1].GetMeta()[feeLineMetaTarget]; got != feeLineTargetWallet {
		t.Fatalf("expected conversion line target %q, got %q", feeLineTargetWallet, got)
	}
	if got, want := quote.GetFeeLines()[1].GetMeta()[feeLineMetaWalletRef], "mw_src_1"; got != want {
		t.Fatalf("unexpected conversion fee wallet_ref: got=%q want=%q", got, want)
	}
	if got, want := len(quote.GetFeeRules()), 2; got != want {
		t.Fatalf("unexpected fee rules count: got=%d want=%d", got, want)
	}
}

func TestBuildPaymentQuote_ConversionFeeReturnedEmptyDoesNotAddLines(t *testing.T) {
	feeClient := &stubFeeEngineClient{
		precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
			"payments.orchestrator.quote": {
				Lines: []*feesv1.DerivedPostingLine{
					testFeeLine("1.00", "USDT"),
				},
			},
			"payments.orchestrator.conversion_quote": {},
		},
	}

	svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
	req := &quoteRequest{
		Meta:           &sharedv1.RequestMeta{OrganizationRef: "org_1"},
		IdempotencyKey: "idem_1",
		Intent:         testManagedWalletToCardIntent(),
	}

	quote, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
	if err != nil {
		t.Fatalf("buildPaymentQuote returned error: %v", err)
	}
	if quote == nil {
		t.Fatalf("expected quote")
	}

	if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.conversion_quote"), 1; got != want {
		t.Fatalf("unexpected conversion precompute calls: got=%d want=%d", got, want)
	}
	if got, want := len(quote.GetFeeLines()), 1; got != want {
		t.Fatalf("unexpected fee lines count: got=%d want=%d", got, want)
	}
	if quote.GetFeeLines()[0].GetMeta()[feeLineMetaTarget] == feeLineTargetWallet {
		t.Fatalf("base fee line should not be tagged as wallet conversion line")
	}
}

func TestBuildPaymentQuote_DoesNotRequestConversionFeesForManagedWalletToLedger(t *testing.T) {
	feeClient := &stubFeeEngineClient{
		precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
			"payments.orchestrator.quote": {
				Lines: []*feesv1.DerivedPostingLine{
					testFeeLine("1.00", "USDT"),
				},
			},
		},
	}

	svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
	req := &quoteRequest{
		Meta:           &sharedv1.RequestMeta{OrganizationRef: "org_1"},
		IdempotencyKey: "idem_1",
		Intent:         testManagedWalletToLedgerIntent(),
	}

	quote, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
	if err != nil {
		t.Fatalf("buildPaymentQuote returned error: %v", err)
	}
	if quote == nil {
		t.Fatalf("expected quote")
	}

	if got, want := len(feeClient.precomputeReqs), 1; got != want {
		t.Fatalf("unexpected precompute call count: got=%d want=%d", got, want)
	}
	if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.conversion_quote"), 0; got != want {
		t.Fatalf("unexpected conversion precompute calls: got=%d want=%d", got, want)
	}
}

func TestBuildPaymentQuote_ReturnsErrorWhenFeeEngineUnavailable(t *testing.T) {
	svc := NewService(zap.NewNop(), nil)
	req := &quoteRequest{
		Meta:           &sharedv1.RequestMeta{OrganizationRef: "org_1"},
		IdempotencyKey: "idem_1",
		Intent:         testManagedWalletToCardIntent(),
	}

	_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
	if err == nil {
		t.Fatalf("expected error")
	}
	if !errors.Is(err, merrors.ErrInternal) {
		t.Fatalf("expected internal error, got: %v", err)
	}
	if !strings.Contains(err.Error(), "fees_precompute_failed") {
		t.Fatalf("expected fees_precompute_failed error, got: %v", err)
	}
}

func TestBuildPaymentQuote_ReturnsErrorWhenBaseFeePrecomputeFails(t *testing.T) {
	feeClient := &stubFeeEngineClient{
		precomputeErrByOrigin: map[string]error{
			"payments.orchestrator.quote": merrors.Internal("billing_fees_unreachable"),
		},
	}

	svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
	req := &quoteRequest{
		Meta:           &sharedv1.RequestMeta{OrganizationRef: "org_1"},
		IdempotencyKey: "idem_1",
		Intent:         testManagedWalletToLedgerIntent(),
	}

	_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
	if err == nil {
		t.Fatalf("expected error")
	}
	if !errors.Is(err, merrors.ErrInternal) {
		t.Fatalf("expected internal error, got: %v", err)
	}
	if !strings.Contains(err.Error(), "fees_precompute_failed") {
		t.Fatalf("expected fees_precompute_failed error, got: %v", err)
	}
}

func TestBuildPaymentQuote_ReturnsErrorWhenConversionFeePrecomputeFails(t *testing.T) {
	feeClient := &stubFeeEngineClient{
		precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
			"payments.orchestrator.quote": {
				Lines: []*feesv1.DerivedPostingLine{
					testFeeLine("1.00", "USDT"),
				},
			},
		},
		precomputeErrByOrigin: map[string]error{
			"payments.orchestrator.conversion_quote": merrors.Internal("billing_fees_unreachable"),
		},
	}

	svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
	req := &quoteRequest{
		Meta:           &sharedv1.RequestMeta{OrganizationRef: "org_1"},
		IdempotencyKey: "idem_1",
		Intent:         testManagedWalletToCardIntent(),
	}

	_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
	if err == nil {
		t.Fatalf("expected error")
	}
	if !errors.Is(err, merrors.ErrInternal) {
		t.Fatalf("expected internal error, got: %v", err)
	}
	if !strings.Contains(err.Error(), "fees_precompute_failed") {
		t.Fatalf("expected fees_precompute_failed error, got: %v", err)
	}
}

type stubFeeEngineClient struct {
	precomputeByOrigin    map[string]*feesv1.PrecomputeFeesResponse
	precomputeErrByOrigin map[string]error
	precomputeReqs        []*feesv1.PrecomputeFeesRequest
}

func (s *stubFeeEngineClient) QuoteFees(context.Context, *feesv1.QuoteFeesRequest, ...grpc.CallOption) (*feesv1.QuoteFeesResponse, error) {
	return nil, merrors.InvalidArgument("unexpected QuoteFees call")
}

func (s *stubFeeEngineClient) PrecomputeFees(_ context.Context, in *feesv1.PrecomputeFeesRequest, _ ...grpc.CallOption) (*feesv1.PrecomputeFeesResponse, error) {
	if s == nil {
		return &feesv1.PrecomputeFeesResponse{}, nil
	}
	if in != nil {
		if cloned, ok := proto.Clone(in).(*feesv1.PrecomputeFeesRequest); ok {
			s.precomputeReqs = append(s.precomputeReqs, cloned)
		} else {
			s.precomputeReqs = append(s.precomputeReqs, in)
		}
	}
	if in == nil || in.GetIntent() == nil {
		return &feesv1.PrecomputeFeesResponse{}, nil
	}

	originType := strings.TrimSpace(in.GetIntent().GetOriginType())
	if err := s.precomputeErrByOrigin[originType]; err != nil {
		return nil, err
	}
	resp, ok := s.precomputeByOrigin[originType]
	if !ok || resp == nil {
		return &feesv1.PrecomputeFeesResponse{}, nil
	}
	if cloned, ok := proto.Clone(resp).(*feesv1.PrecomputeFeesResponse); ok {
		return cloned, nil
	}
	return resp, nil
}

func (s *stubFeeEngineClient) ValidateFeeToken(context.Context, *feesv1.ValidateFeeTokenRequest, ...grpc.CallOption) (*feesv1.ValidateFeeTokenResponse, error) {
	return nil, merrors.InvalidArgument("unexpected ValidateFeeToken call")
}

func precomputeCallsByOrigin(reqs []*feesv1.PrecomputeFeesRequest, originType string) int {
	count := 0
	for _, req := range reqs {
		if req == nil || req.GetIntent() == nil {
			continue
		}
		if strings.EqualFold(strings.TrimSpace(req.GetIntent().GetOriginType()), strings.TrimSpace(originType)) {
			count++
		}
	}
	return count
}

func testFeeLine(amount, currency string) *feesv1.DerivedPostingLine {
	return &feesv1.DerivedPostingLine{
		Money: &moneyv1.Money{
			Amount:   amount,
			Currency: currency,
		},
		LineType: accountingv1.PostingLineType_POSTING_LINE_FEE,
		Side:     accountingv1.EntrySide_ENTRY_SIDE_DEBIT,
	}
}

func testManagedWalletToCardIntent() *sharedv1.PaymentIntent {
	return &sharedv1.PaymentIntent{
		Kind: sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT,
		Source: &sharedv1.PaymentEndpoint{
			Endpoint: &sharedv1.PaymentEndpoint_ManagedWallet{
				ManagedWallet: &sharedv1.ManagedWalletEndpoint{
					ManagedWalletRef: "mw_src_1",
				},
			},
		},
		Destination: &sharedv1.PaymentEndpoint{
			Endpoint: &sharedv1.PaymentEndpoint_Card{
				Card: &sharedv1.CardEndpoint{
					Card: &sharedv1.CardEndpoint_Pan{Pan: "4111111111111111"},
				},
			},
		},
		Amount: &moneyv1.Money{
			Amount:   "100",
			Currency: "USDT",
		},
	}
}

func testManagedWalletToLedgerIntent() *sharedv1.PaymentIntent {
	return &sharedv1.PaymentIntent{
		Kind: sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT,
		Source: &sharedv1.PaymentEndpoint{
			Endpoint: &sharedv1.PaymentEndpoint_ManagedWallet{
				ManagedWallet: &sharedv1.ManagedWalletEndpoint{
					ManagedWalletRef: "mw_src_1",
				},
			},
		},
		Destination: &sharedv1.PaymentEndpoint{
			Endpoint: &sharedv1.PaymentEndpoint_Ledger{
				Ledger: &sharedv1.LedgerEndpoint{
					LedgerAccountRef: "ledger_dst_1",
				},
			},
		},
		Amount: &moneyv1.Money{
			Amount:   "100",
			Currency: "USDT",
		},
	}
}
