package oracle

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/tech/sendico/fx/storage"
	"github.com/tech/sendico/fx/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	"go.uber.org/zap"
)

type repositoryStub struct {
	rates      storage.RatesStore
	quotes     storage.QuotesStore
	pairs      storage.PairStore
	currencies storage.CurrencyStore
	pingErr    error
}

func (r *repositoryStub) Ping(ctx context.Context) error { return r.pingErr }
func (r *repositoryStub) Rates() storage.RatesStore      { return r.rates }
func (r *repositoryStub) Quotes() storage.QuotesStore    { return r.quotes }
func (r *repositoryStub) Pairs() storage.PairStore       { return r.pairs }
func (r *repositoryStub) Currencies() storage.CurrencyStore {
	return r.currencies
}

type ratesStoreStub struct {
	latestFn func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error)
}

func (r *ratesStoreStub) UpsertSnapshot(ctx context.Context, snapshot *model.RateSnapshot) error {
	return nil
}

func (r *ratesStoreStub) LatestSnapshot(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
	if r.latestFn != nil {
		return r.latestFn(ctx, pair, provider)
	}
	return nil, merrors.ErrNoData
}

type quotesStoreStub struct {
	issueFn   func(ctx context.Context, quote *model.Quote) error
	getFn     func(ctx context.Context, ref string) (*model.Quote, error)
	consumeFn func(ctx context.Context, ref, ledger string, when time.Time) (*model.Quote, error)
}

func (q *quotesStoreStub) Issue(ctx context.Context, quote *model.Quote) error {
	if q.issueFn != nil {
		return q.issueFn(ctx, quote)
	}
	return nil
}

func (q *quotesStoreStub) GetByRef(ctx context.Context, ref string) (*model.Quote, error) {
	if q.getFn != nil {
		return q.getFn(ctx, ref)
	}
	return nil, merrors.ErrNoData
}

func (q *quotesStoreStub) Consume(ctx context.Context, ref, ledger string, when time.Time) (*model.Quote, error) {
	if q.consumeFn != nil {
		return q.consumeFn(ctx, ref, ledger, when)
	}
	return nil, merrors.ErrNoData
}

func (q *quotesStoreStub) ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) {
	return 0, nil
}

type pairStoreStub struct {
	getFn  func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error)
	listFn func(ctx context.Context) ([]*model.Pair, error)
}

func (p *pairStoreStub) ListEnabled(ctx context.Context) ([]*model.Pair, error) {
	if p.listFn != nil {
		return p.listFn(ctx)
	}
	return nil, nil
}

func (p *pairStoreStub) Get(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
	if p.getFn != nil {
		return p.getFn(ctx, pair)
	}
	return nil, merrors.ErrNoData
}

func (p *pairStoreStub) Upsert(ctx context.Context, pair *model.Pair) error { return nil }

type currencyStoreStub struct{}

func (currencyStoreStub) Get(ctx context.Context, code string) (*model.Currency, error) {
	return nil, merrors.ErrNoData
}
func (currencyStoreStub) List(ctx context.Context, codes ...string) ([]*model.Currency, error) {
	return nil, nil
}
func (currencyStoreStub) Upsert(ctx context.Context, currency *model.Currency) error { return nil }

func TestServiceGetQuoteFirm(t *testing.T) {
	pricedAt := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
	repo := &repositoryStub{}
	repo.pairs = &pairStoreStub{
		getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
			return &model.Pair{
				Pair:      pair,
				BaseMeta:  model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
			}, nil
		},
	}
	repo.rates = &ratesStoreStub{
		latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
			return &model.RateSnapshot{
				Pair:       pair,
				Provider:   provider,
				Ask:        "1.10",
				Bid:        "1.08",
				RateRef:    "rate#1",
				AsOfUnixMs: pricedAt.UnixMilli(),
			}, nil
		},
	}
	savedQuote := &model.Quote{}
	repo.quotes = &quotesStoreStub{
		issueFn: func(ctx context.Context, quote *model.Quote) error {
			*savedQuote = *quote
			return nil
		},
	}
	repo.currencies = currencyStoreStub{}

	svc := NewService(zap.NewNop(), repo, nil, "")

	req := &oraclev1.GetQuoteRequest{
		Meta: &oraclev1.RequestMeta{
			TenantRef: "tenant",
			Trace:     &tracev1.TraceContext{RequestRef: "req"},
		},
		Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"},
		Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
		AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{
			Currency: "USD",
			Amount:   "100",
		}},
		Firm:  true,
		TtlMs: 60000,
	}

	resp, err := svc.GetQuote(context.Background(), req)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.GetQuote().GetFirm() != true {
		t.Fatalf("expected firm quote")
	}
	if resp.GetQuote().GetQuoteAmount().GetAmount() != "110.00" {
		t.Fatalf("unexpected quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount())
	}
	if got := resp.GetQuote().GetPricedAt(); got == nil || !got.AsTime().Equal(pricedAt) {
		t.Fatalf("expected priced_at %s, got %v", pricedAt, got)
	}
	if savedQuote.QuoteRef == "" {
		t.Fatalf("expected quote persisted")
	}
	if savedQuote.PricedAtUnixMs != pricedAt.UnixMilli() {
		t.Fatalf("expected stored pricedAtUnixMs %d, got %d", pricedAt.UnixMilli(), savedQuote.PricedAtUnixMs)
	}
}

func TestServiceGetQuoteFirm_ClampsTTLToConfiguredMax(t *testing.T) {
	const (
		configuredMaxTTL = 1 * time.Second
		requestedTTL     = 1 * time.Minute
	)

	repo := &repositoryStub{}
	repo.pairs = &pairStoreStub{
		getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
			return &model.Pair{
				Pair:      pair,
				BaseMeta:  model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
			}, nil
		},
	}
	repo.rates = &ratesStoreStub{
		latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
			return &model.RateSnapshot{
				Pair:       pair,
				Provider:   provider,
				Ask:        "1.10",
				Bid:        "1.08",
				RateRef:    "rate#1",
				AsOfUnixMs: time.Now().UnixMilli(),
			}, nil
		},
	}
	repo.quotes = &quotesStoreStub{}
	repo.currencies = currencyStoreStub{}

	svc := NewService(zap.NewNop(), repo, nil, "", WithMaxQuoteTTLMillis(int64(configuredMaxTTL/time.Millisecond)))
	start := time.Now()

	resp, err := svc.GetQuote(context.Background(), &oraclev1.GetQuoteRequest{
		Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"},
		Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
		AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{
			Currency: "USD",
			Amount:   "100",
		}},
		Firm:  true,
		TtlMs: int64(requestedTTL / time.Millisecond),
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	expiry := time.UnixMilli(resp.GetQuote().GetExpiresAtUnixMs())
	if expiry.Before(start) {
		t.Fatalf("expected expiry after request start, got %s", expiry)
	}
	if expiry.After(start.Add(5 * time.Second)) {
		t.Fatalf("expected clamped expiry close to 1s max ttl, got %s", expiry)
	}
}

func TestServiceGetQuoteRateNotFound(t *testing.T) {
	repo := &repositoryStub{
		pairs: &pairStoreStub{
			getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
				return &model.Pair{
					Pair:      pair,
					BaseMeta:  model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
					QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				}, nil
			},
		},
		rates: &ratesStoreStub{latestFn: func(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) {
			return nil, merrors.ErrNoData
		}},
	}
	svc := NewService(zap.NewNop(), repo, nil, "")

	_, err := svc.GetQuote(context.Background(), &oraclev1.GetQuoteRequest{
		Pair:        &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"},
		Side:        fxv1.Side_BUY_BASE_SELL_QUOTE,
		AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{Currency: "USD", Amount: "1"}},
	})
	if err == nil {
		t.Fatalf("expected error")
	}
}

func TestServiceGetQuoteCrossRate(t *testing.T) {
	repo := &repositoryStub{}
	targetPair := model.CurrencyPair{Base: "EUR", Quote: "RUB"}
	baseLegPair := model.CurrencyPair{Base: "USDT", Quote: "EUR"}
	quoteLegPair := model.CurrencyPair{Base: "USDT", Quote: "RUB"}

	repo.pairs = &pairStoreStub{
		getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
			if pair != targetPair {
				t.Fatalf("unexpected pair lookup: %v", pair)
			}
			return &model.Pair{
				Pair:            pair,
				BaseMeta:        model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				QuoteMeta:       model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				DefaultProvider: "CROSSPROV",
				Cross: &model.CrossRateConfig{
					Enabled: true,
					BaseLeg: model.CrossRateLeg{
						Pair:   baseLegPair,
						Invert: true,
					},
					QuoteLeg: model.CrossRateLeg{
						Pair: quoteLegPair,
					},
				},
			}, nil
		},
	}
	repo.rates = &ratesStoreStub{
		latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
			switch pair {
			case targetPair:
				return nil, merrors.ErrNoData
			case baseLegPair:
				return &model.RateSnapshot{
					Pair:       pair,
					Provider:   provider,
					Ask:        "0.90",
					Bid:        "0.90",
					Mid:        "0.90",
					RateRef:    "base-leg",
					AsOfUnixMs: 1_000,
				}, nil
			case quoteLegPair:
				return &model.RateSnapshot{
					Pair:       pair,
					Provider:   provider,
					Ask:        "90",
					Bid:        "90",
					Mid:        "90",
					RateRef:    "quote-leg",
					AsOfUnixMs: 2_000,
				}, nil
			default:
				return nil, merrors.ErrNoData
			}
		},
	}
	repo.quotes = &quotesStoreStub{}
	repo.currencies = currencyStoreStub{}

	svc := NewService(zap.NewNop(), repo, nil, "")

	req := &oraclev1.GetQuoteRequest{
		Pair:        &fxv1.CurrencyPair{Base: "EUR", Quote: "RUB"},
		Side:        fxv1.Side_BUY_BASE_SELL_QUOTE,
		AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{Currency: "EUR", Amount: "1"}},
	}

	resp, err := svc.GetQuote(context.Background(), req)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.GetQuote().GetPrice().GetValue() != "100.00" {
		t.Fatalf("unexpected cross price: %s", resp.GetQuote().GetPrice().GetValue())
	}
	if resp.GetQuote().GetQuoteAmount().GetAmount() != "100.00" {
		t.Fatalf("unexpected cross quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount())
	}
	if !strings.HasPrefix(resp.GetQuote().GetRateRef(), "cross|") {
		t.Fatalf("expected cross rate ref, got %s", resp.GetQuote().GetRateRef())
	}
	if resp.GetQuote().GetProvider() != "CROSSPROV" {
		t.Fatalf("unexpected provider: %s", resp.GetQuote().GetProvider())
	}

}

func TestServiceLatestRateCross(t *testing.T) {
	repo := &repositoryStub{}
	targetPair := model.CurrencyPair{Base: "EUR", Quote: "RUB"}
	baseLegPair := model.CurrencyPair{Base: "USDT", Quote: "EUR"}
	quoteLegPair := model.CurrencyPair{Base: "USDT", Quote: "RUB"}

	repo.pairs = &pairStoreStub{
		getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
			if pair != targetPair {
				t.Fatalf("unexpected pair lookup: %v", pair)
			}
			return &model.Pair{
				Pair:            pair,
				BaseMeta:        model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				QuoteMeta:       model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
				DefaultProvider: "CROSSPROV",
				Cross: &model.CrossRateConfig{
					Enabled: true,
					BaseLeg: model.CrossRateLeg{
						Pair:   baseLegPair,
						Invert: true,
					},
					QuoteLeg: model.CrossRateLeg{
						Pair: quoteLegPair,
					},
				},
			}, nil
		},
	}
	repo.rates = &ratesStoreStub{
		latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
			switch pair {
			case targetPair:
				return nil, merrors.ErrNoData
			case baseLegPair:
				return &model.RateSnapshot{
					Pair:       pair,
					Provider:   provider,
					Ask:        "0.90",
					Bid:        "0.90",
					Mid:        "0.90",
					RateRef:    "base-leg",
					AsOfUnixMs: 1_000,
				}, nil
			case quoteLegPair:
				return &model.RateSnapshot{
					Pair:       pair,
					Provider:   provider,
					Ask:        "90",
					Bid:        "90",
					Mid:        "90",
					RateRef:    "quote-leg",
					AsOfUnixMs: 2_000,
				}, nil
			default:
				return nil, merrors.ErrNoData
			}
		},
	}
	repo.quotes = &quotesStoreStub{}
	repo.currencies = currencyStoreStub{}

	svc := NewService(zap.NewNop(), repo, nil, "")

	resp, err := svc.LatestRate(context.Background(), &oraclev1.LatestRateRequest{
		Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "RUB"},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if resp.GetRate().GetMid().GetValue() != "100.00000000" {
		t.Fatalf("unexpected mid price: %s", resp.GetRate().GetMid().GetValue())
	}
	if resp.GetRate().GetProvider() != "CROSSPROV" {
		t.Fatalf("unexpected provider: %s", resp.GetRate().GetProvider())
	}
	if !strings.HasPrefix(resp.GetRate().GetRateRef(), "cross|") {
		t.Fatalf("expected cross rate ref, got %s", resp.GetRate().GetRateRef())
	}
}

func TestServiceValidateQuote(t *testing.T) {
	now := time.Now().Add(time.Minute)
	repo := &repositoryStub{
		quotes: &quotesStoreStub{
			getFn: func(context.Context, string) (*model.Quote, error) {
				return &model.Quote{
					QuoteRef:        "q1",
					Pair:            model.CurrencyPair{Base: "USD", Quote: "EUR"},
					Side:            model.QuoteSideBuyBaseSellQuote,
					Price:           "1.10",
					BaseAmount:      paymenttypes.Money{Currency: "USD", Amount: "100"},
					QuoteAmount:     paymenttypes.Money{Currency: "EUR", Amount: "110"},
					ExpiresAtUnixMs: now.UnixMilli(),
					Status:          model.QuoteStatusIssued,
				}, nil
			},
		},
	}
	svc := NewService(zap.NewNop(), repo, nil, "")

	resp, err := svc.ValidateQuote(context.Background(), &oraclev1.ValidateQuoteRequest{QuoteRef: "q1"})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !resp.GetValid() {
		t.Fatalf("expected quote valid")
	}
}

func TestServiceConsumeQuoteExpired(t *testing.T) {
	repo := &repositoryStub{
		quotes: &quotesStoreStub{
			consumeFn: func(context.Context, string, string, time.Time) (*model.Quote, error) {
				return nil, storage.ErrQuoteExpired
			},
		},
	}
	svc := NewService(zap.NewNop(), repo, nil, "")

	_, err := svc.ConsumeQuote(context.Background(), &oraclev1.ConsumeQuoteRequest{QuoteRef: "q1", LedgerTxnRef: "ledger"})
	if err == nil {
		t.Fatalf("expected error")
	}
}

func TestServiceLatestRateSuccess(t *testing.T) {
	repo := &repositoryStub{
		rates: &ratesStoreStub{latestFn: func(_ context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
			if pair != (model.CurrencyPair{Base: "USD", Quote: "EUR"}) {
				t.Fatalf("unexpected pair: %v", pair)
			}
			if provider != "DEFAULT" {
				t.Fatalf("unexpected provider: %s", provider)
			}
			return &model.RateSnapshot{Pair: pair, RateRef: "rate", Provider: provider}, nil
		}},
		pairs: &pairStoreStub{
			getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
				return &model.Pair{
					Pair:            pair,
					BaseMeta:        model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
					QuoteMeta:       model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
					DefaultProvider: "DEFAULT",
				}, nil
			},
		},
	}
	svc := NewService(zap.NewNop(), repo, nil, "")

	resp, err := svc.LatestRate(context.Background(), &oraclev1.LatestRateRequest{Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.GetRate().GetRateRef() != "rate" {
		t.Fatalf("unexpected rate ref")
	}
}

func TestServiceListPairs(t *testing.T) {
	repo := &repositoryStub{
		pairs: &pairStoreStub{listFn: func(context.Context) ([]*model.Pair, error) {
			return []*model.Pair{{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}}, nil
		}},
	}
	svc := NewService(zap.NewNop(), repo, nil, "")

	resp, err := svc.ListPairs(context.Background(), &oraclev1.ListPairsRequest{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(resp.GetPairs()) != 1 {
		t.Fatalf("expected one pair")
	}
}
