package payment

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"testing"
	"time"
)

type paymentRepoStub struct {
	create      *PaymentResult
	createdReq  *PaymentRequest
	refund      *PaymentResult
	refundErr   error
	refundedReq *PaymentRefundRequest
	callback    *PaymentResult
	query       *PaymentResult
	queryCalls  int
	callbackErr error
}

func (r *paymentRepoStub) Create(_ context.Context, req *PaymentRequest) (*PaymentResult, error) {
	copyReq := *req
	r.createdReq = &copyReq
	return r.create, nil
}

type paymentOrderRepoStub struct {
	order *PaymentOrder
}

func (r *paymentOrderRepoStub) CreatePaymentOrder(_ context.Context, order *PaymentOrder) (*PaymentOrder, bool, error) {
	if r.order != nil {
		return r.order, false, nil
	}
	copyOrder := *order
	r.order = &copyOrder
	return r.order, true, nil
}
func (r *paymentOrderRepoStub) FindPaymentOrder(context.Context, string, string) (*PaymentOrder, error) {
	if r.order == nil {
		return nil, ErrPaymentOrderNotFound
	}
	return r.order, nil
}
func (r *paymentOrderRepoStub) ListPaymentOrders(context.Context, int, int, PaymentOrderFilter) ([]*PaymentOrder, int64, error) {
	if r.order == nil {
		return []*PaymentOrder{}, 0, nil
	}
	return []*PaymentOrder{r.order}, 1, nil
}
func (r *paymentOrderRepoStub) RecordPaymentCreate(_ context.Context, _, _ string, update *PaymentProviderUpdate) (*PaymentOrder, error) {
	r.order.PaymentStatus = PaymentStatusPending
	r.order.CreatePayload = append(r.order.CreatePayload[:0], update.CreatePayload...)
	return r.order, nil
}
func (r *paymentOrderRepoStub) ApplyPaymentResult(_ context.Context, _, _ string, update *PaymentProviderUpdate) (*PaymentOrder, error) {
	if update.Status == "success" {
		r.order.PaymentStatus = PaymentStatusPaid
		r.order.PaidAmount = r.order.Amount
		r.order.ProviderTradeNo = update.ProviderTradeNo
	}
	return r.order, nil
}
func (r *paymentOrderRepoStub) BeginPaymentFulfillment(context.Context, string, string, time.Duration) (*PaymentOrder, string, bool, error) {
	if r.order.FulfillmentStatus == FulfillmentStatusSucceeded {
		return r.order, "", true, nil
	}
	r.order.FulfillmentStatus = FulfillmentStatusProcessing
	return r.order, "token", false, nil
}
func (r *paymentOrderRepoStub) CompletePaymentFulfillment(_ context.Context, _, _, _ string, success bool, _ string) (*PaymentOrder, error) {
	if success {
		r.order.FulfillmentStatus = FulfillmentStatusSucceeded
	} else {
		r.order.FulfillmentStatus = FulfillmentStatusFailed
	}
	return r.order, nil
}
func (r *paymentOrderRepoStub) BeginPaymentRefund(context.Context, string, string, int64, time.Duration) (*PaymentOrder, string, error) {
	r.order.RefundNo = "refund-1"
	r.order.RefundStatus = RefundStatusProcessing
	return r.order, "refund-token", nil
}
func (r *paymentOrderRepoStub) CompletePaymentRefundRequest(context.Context, string, string, string, bool, string) (*PaymentOrder, error) {
	r.order.RefundStatus = RefundStatusPending
	return r.order, nil
}
func (r *paymentOrderRepoStub) ConfirmPaymentRefund(context.Context, string, string, string, int64, bool, string) (*PaymentOrder, error) {
	return r.order, nil
}

type paymentSourceStub struct{ intent *PaymentIntent }

func (s *paymentSourceStub) Type() string { return s.intent.BusinessType }
func (s *paymentSourceStub) PreparePayment(context.Context, string, string, string) (*PaymentIntent, error) {
	return s.intent, nil
}
func (r *paymentRepoStub) Query(context.Context, string, string) (*PaymentResult, error) {
	r.queryCalls++
	return r.query, nil
}
func (r *paymentRepoStub) Refund(_ context.Context, req *PaymentRefundRequest) (*PaymentResult, error) {
	if req != nil {
		copyReq := *req
		r.refundedReq = &copyReq
	}
	return r.refund, r.refundErr
}
func (r *paymentRepoStub) HandleCallback(context.Context, *PaymentCallback) (*PaymentResult, error) {
	return r.callback, r.callbackErr
}

type paymentFulfillmentStub struct {
	calls      int
	failFirst  bool
	confirmIDs []string
}

func (h *paymentFulfillmentStub) Type() string { return "game_item" }
func (h *paymentFulfillmentStub) AuthorizeRefund(context.Context, *PaymentOrder, int64) error {
	return nil
}
func (h *paymentFulfillmentStub) Fulfill(_ context.Context, confirmation *PaymentConfirmation) error {
	h.calls++
	h.confirmIDs = append(h.confirmIDs, confirmation.ID)
	if h.failFirst && h.calls == 1 {
		return errors.New("temporary fulfillment failure")
	}
	return nil
}

func newPaymentUsecaseForTest(repo PaymentRepo) *PaymentUsecase {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, logger)
}

func testBizPaymentOrder() *PaymentOrder {
	return &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPending, FulfillmentStatus: FulfillmentStatusPending, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}
}

func successPaymentResult() *PaymentResult {
	return &PaymentResult{
		Provider:        PaymentAlipay,
		Status:          "success",
		TradeNo:         "order-1",
		ProviderTradeNo: "ali-1",
		Amount:          100,
		Currency:        "CNY",
	}
}

func successIntent() *PaymentIntent {
	return &PaymentIntent{
		Provider:     PaymentAlipay,
		TradeNo:      "order-1",
		BusinessType: "game_item",
		BusinessID:   "item-1",
		Amount:       100,
		Currency:     "CNY",
	}
}

func TestPaymentCallbackQueriesBeforeFulfillment(t *testing.T) {
	repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
	uc := newPaymentUsecaseForTest(repo)
	_, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
	if err == nil {
		t.Fatal("callback succeeded without fulfillment handler")
	}
	if repo.queryCalls != 1 {
		t.Fatalf("query calls = %d, want 1", repo.queryCalls)
	}
}

func TestPaymentCallbackPendingDoesNotFulfill(t *testing.T) {
	pending := successPaymentResult()
	pending.Status = "pending"
	repo := &paymentRepoStub{callback: successPaymentResult(), query: pending}
	handler := &paymentFulfillmentStub{}
	uc := newPaymentUsecaseForTest(repo)
	registry := NewPaymentFulfillmentRegistry()
	if err := registry.Register(handler); err != nil {
		t.Fatal(err)
	}
	uc.SetFulfillmentRegistry(registry)
	result, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
	if err != nil {
		t.Fatal(err)
	}
	if result.Status != "pending" || handler.calls != 0 {
		t.Fatalf("result=%+v handler=%d", result, handler.calls)
	}
}

func TestPaymentCallbackRejectsAmountMismatch(t *testing.T) {
	repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
	handler := &paymentFulfillmentStub{}
	uc := newPaymentUsecaseForTest(repo)
	uc.orders.(*paymentOrderRepoStub).order.Amount = 101
	registry := NewPaymentFulfillmentRegistry()
	_ = registry.Register(handler)
	uc.SetFulfillmentRegistry(registry)
	if _, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}); err == nil {
		t.Fatal("amount mismatch was accepted")
	}
	if handler.calls != 0 {
		t.Fatal("handler ran after amount mismatch")
	}
}

func TestPaymentCallbackRequiresFulfillmentHandler(t *testing.T) {
	repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
	uc := newPaymentUsecaseForTest(repo)
	if _, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}); err == nil {
		t.Fatal("callback succeeded without fulfillment handler")
	}
}

func TestPaymentCallbackDuplicateDoesNotFulfillTwice(t *testing.T) {
	repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
	handler := &paymentFulfillmentStub{}
	uc := newPaymentUsecaseForTest(repo)
	registry := NewPaymentFulfillmentRegistry()
	_ = registry.Register(handler)
	uc.SetFulfillmentRegistry(registry)
	callback := &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}
	if _, err := uc.Callback(context.Background(), callback); err != nil {
		t.Fatal(err)
	}
	second, err := uc.Callback(context.Background(), callback)
	if err != nil {
		t.Fatal(err)
	}
	if !second.Duplicate || handler.calls != 1 || repo.queryCalls != 2 {
		t.Fatalf("duplicate=%v handler=%d query=%d", second.Duplicate, handler.calls, repo.queryCalls)
	}
}

func TestPaymentCallbackFulfillmentFailureCanRetry(t *testing.T) {
	repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
	handler := &paymentFulfillmentStub{failFirst: true}
	uc := newPaymentUsecaseForTest(repo)
	registry := NewPaymentFulfillmentRegistry()
	_ = registry.Register(handler)
	uc.SetFulfillmentRegistry(registry)
	callback := &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}
	if _, err := uc.Callback(context.Background(), callback); err == nil {
		t.Fatal("first fulfillment failure was accepted")
	}
	result, err := uc.Callback(context.Background(), callback)
	if err != nil {
		t.Fatal(err)
	}
	if result.Status != "fulfilled" || handler.calls != 2 || handler.confirmIDs[0] != handler.confirmIDs[1] {
		t.Fatalf("result=%+v handler=%d ids=%v", result, handler.calls, handler.confirmIDs)
	}
}

func TestPaymentQueryRejectsIncompleteSuccess(t *testing.T) {
	result := successPaymentResult()
	result.ProviderTradeNo = ""
	uc := newPaymentUsecaseForTest(&paymentRepoStub{query: result})
	if _, err := uc.Query(context.Background(), PaymentAlipay, "order-1"); err == nil {
		t.Fatal("incomplete successful query was accepted")
	}
}

func TestPaymentQueryRejectsUnknownStatus(t *testing.T) {
	result := successPaymentResult()
	result.Status = "maybe"
	uc := newPaymentUsecaseForTest(&paymentRepoStub{query: result})
	if _, err := uc.Query(context.Background(), PaymentAlipay, "order-1"); err == nil {
		t.Fatal("unknown payment status was accepted")
	}
}

func TestPaymentCallbackErrorCarriesSafeAck(t *testing.T) {
	ack := DefaultPaymentCallbackAck(PaymentAlipay, false)
	err := callbackResultError(&PaymentResult{FailureAck: ack}, errors.New("internal order resolver details"))
	callbackErr, ok := err.(*PaymentCallbackError)
	if !ok || string(callbackErr.Ack.Body) != "failure" {
		t.Fatalf("callback error = %#v", err)
	}
}

func TestPersistentPaymentCreateUsesBusinessCanonicalAmount(t *testing.T) {
	repo := &paymentRepoStub{create: &PaymentResult{Provider: PaymentAlipay, Status: "created", TradeNo: "order-1", Payload: []byte(`{"prepay":"x"}`)}}
	orders := &paymentOrderRepoStub{}
	uc := NewPaymentUsecase(repo, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
	sources := NewPaymentOrderSourceRegistry()
	if err := sources.Register(&paymentSourceStub{intent: &PaymentIntent{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "canonical item", Amount: 100, Currency: "CNY", Extra: []byte(`{"product_id":"canonical"}`)}}); err != nil {
		t.Fatal(err)
	}
	uc.SetOrderSourceRegistry(sources)
	result, err := uc.Create(context.Background(), &PaymentRequest{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "forged", Amount: 1, Currency: "USD", Extra: map[string]any{"product_id": "forged"}})
	if err != nil {
		t.Fatal(err)
	}
	if repo.createdReq.Amount != 100 || repo.createdReq.Currency != "CNY" || repo.createdReq.Subject != "canonical item" || repo.createdReq.Extra["product_id"] != "canonical" || orders.order.Amount != 100 || result.OrderStatus != PaymentStatusPending {
		t.Fatalf("canonical order was not used: request=%+v order=%+v result=%+v", repo.createdReq, orders.order, result)
	}
}

func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
	repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
	orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPending, FulfillmentStatus: FulfillmentStatusPending, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
	handler := &paymentFulfillmentStub{}
	uc := NewPaymentUsecase(repo, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
	registry := NewPaymentFulfillmentRegistry()
	if err := registry.Register(handler); err != nil {
		t.Fatal(err)
	}
	uc.SetFulfillmentRegistry(registry)
	first, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
	if err != nil {
		t.Fatal(err)
	}
	second, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
	if err != nil {
		t.Fatal(err)
	}
	if first.Status != "fulfilled" || second.Status != "fulfilled" || !second.Duplicate || handler.calls != 1 || orders.order.FulfillmentStatus != FulfillmentStatusSucceeded {
		t.Fatalf("persistent callback flow failed: first=%+v second=%+v handler=%d order=%+v", first, second, handler.calls, orders.order)
	}
}

func TestPaymentFulfillRetriesFailedOrder(t *testing.T) {
	orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPaid, FulfillmentStatus: FulfillmentStatusFailed, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
	uc := NewPaymentUsecase(&paymentRepoStub{}, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
	handler := &paymentFulfillmentStub{}
	registry := NewPaymentFulfillmentRegistry()
	if err := registry.Register(handler); err != nil {
		t.Fatal(err)
	}
	uc.SetFulfillmentRegistry(registry)
	result, err := uc.Fulfill(context.Background(), PaymentAlipay, "order-1")
	if err != nil {
		t.Fatal(err)
	}
	if result.Status != "fulfilled" || handler.calls != 1 || orders.order.FulfillmentStatus != FulfillmentStatusSucceeded {
		t.Fatalf("fulfillment retry failed: result=%+v calls=%d status=%s", result, handler.calls, orders.order.FulfillmentStatus)
	}
}

func TestPaymentConfirmationAcceptsProviderDiscount(t *testing.T) {
	intent := &PaymentIntent{Provider: PaymentAlipay, TradeNo: "T1", Amount: 1000, Currency: "CNY", BusinessType: "item", BusinessID: "I1"}
	result := &PaymentResult{Provider: PaymentAlipay, TradeNo: "T1", ProviderTradeNo: "P1", Status: "success", Amount: 1000, PayerPaidAmount: 800, CashPaidAmount: 800, DiscountAmount: 200, ProviderDiscountAmount: 150, MerchantDiscountAmount: 50, SettlementAmount: 950, Currency: "CNY", AmountBreakdownKnown: true}
	if err := validatePaymentConfirmation(intent, PaymentAlipay, "T1", result); err != nil {
		t.Fatal(err)
	}
}

func TestPaymentConfirmationRejectsOverpayment(t *testing.T) {
	intent := &PaymentIntent{Provider: PaymentWechatV3, TradeNo: "T1", Amount: 1000, Currency: "CNY", BusinessType: "item", BusinessID: "I1"}
	result := &PaymentResult{Provider: PaymentWechatV3, TradeNo: "T1", ProviderTradeNo: "P1", Status: "success", Amount: 1000, PayerPaidAmount: 1001, CashPaidAmount: 1001, Currency: "CNY", AmountBreakdownKnown: true}
	if err := validatePaymentConfirmation(intent, PaymentWechatV3, "T1", result); err == nil {
		t.Fatal("overpayment was accepted")
	}
}

func TestPaymentConfirmationRejectsBrokenDiscountConservation(t *testing.T) {
	intent := &PaymentIntent{Provider: PaymentWechatV2, TradeNo: "T1", Amount: 1000, Currency: "CNY", BusinessType: "item", BusinessID: "I1"}
	result := &PaymentResult{Provider: PaymentWechatV2, TradeNo: "T1", ProviderTradeNo: "P1", Status: "success", Amount: 1000, PayerPaidAmount: 800, CashPaidAmount: 800, DiscountAmount: 100, Currency: "CNY", AmountBreakdownKnown: true}
	if err := validatePaymentConfirmation(intent, PaymentWechatV2, "T1", result); err == nil {
		t.Fatal("broken discount conservation was accepted")
	}
}

func TestPaymentConfirmationBindsAppleProductID(t *testing.T) {
	intent := &PaymentIntent{
		Provider: PaymentApple, TradeNo: "123e4567-e89b-12d3-a456-426614174000",
		Amount: 100, Currency: "USD", BusinessType: "item", BusinessID: "I1",
		Extra: json.RawMessage(`{"product_id":"com.example.coin100"}`),
	}
	result := &PaymentResult{
		Provider: PaymentApple, TradeNo: intent.TradeNo, ProviderTradeNo: "APPLE-TX-1",
		Status: "success", Amount: 100, Currency: "USD",
		Payload: json.RawMessage(`{"productId":"com.example.coin100"}`),
	}
	if err := validatePaymentConfirmation(intent, PaymentApple, intent.TradeNo, result); err != nil {
		t.Fatalf("matching productId rejected: %v", err)
	}
	result.Payload = json.RawMessage(`{"productId":"com.example.coin200"}`)
	if err := validatePaymentConfirmation(intent, PaymentApple, intent.TradeNo, result); err == nil {
		t.Fatal("mismatched Apple productId was accepted")
	}
}
