package ingestor

import (
	"context"
	"errors"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/tech/sendico/fx/ingestor/internal/config"
	mmarket "github.com/tech/sendico/fx/ingestor/internal/model"
	"github.com/tech/sendico/fx/storage"
	"github.com/tech/sendico/fx/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"go.uber.org/zap"
)

var errNoSnapshot = errors.New("snapshot not found")

func TestParseDecimal(t *testing.T) {
	got, err := parseDecimal("123.456")
	if err != nil {
		t.Fatalf("parseDecimal returned error: %v", err)
	}
	if got.String() != "15432/125" { // 123.456 expressed as a rational
		t.Fatalf("unexpected rational value: %s", got.String())
	}

	if _, err := parseDecimal("not-a-number"); err == nil {
		t.Fatalf("parseDecimal should fail on invalid decimal string")
	}
}

func TestInvertPrices(t *testing.T) {
	bid, err := parseDecimal("2")
	if err != nil {
		t.Fatalf("parseDecimal: %v", err)
	}
	ask, err := parseDecimal("4")
	if err != nil {
		t.Fatalf("parseDecimal: %v", err)
	}

	invBid, invAsk := invertPrices(bid, ask)
	if diff := cmp.Diff("0.5", invAsk.FloatString(1)); diff != "" {
		t.Fatalf("unexpected inverted ask (-want +got):\n%s", diff)
	}
	if diff := cmp.Diff("0.25", invBid.FloatString(2)); diff != "" {
		t.Fatalf("unexpected inverted bid (-want +got):\n%s", diff)
	}
}

func TestServiceUpsertPairStoresSnapshot(t *testing.T) {
	store := &ratesStoreStub{}
	svc := testService(store, map[mmarket.Driver]mmarket.Connector{
		mmarket.DriverBinance: &connectorStub{
			id: mmarket.DriverBinance,
			ticker: &mmarket.Ticker{
				Symbol:   "EURUSDT",
				BidPrice: "1.0000",
				AskPrice: "1.2000",
				Provider: "binance",
			},
		},
	})

	pair := config.Pair{
		PairConfig: config.PairConfig{
			Base:     "USDT",
			Quote:    "EUR",
			Symbol:   "EURUSDT",
			Provider: "binance",
		},
		Source: mmarket.DriverBinance,
	}

	if err := svc.upsertPair(context.Background(), pair); err != nil {
		t.Fatalf("upsertPair returned error: %v", err)
	}
	if len(store.snapshots) != 1 {
		t.Fatalf("expected 1 snapshot stored, got %d", len(store.snapshots))
	}
	snap := store.snapshots[0]
	if snap.Pair.Base != "USDT" || snap.Pair.Quote != "EUR" {
		t.Fatalf("unexpected currency pair stored: %+v", snap.Pair)
	}
	if snap.Provider != "binance" {
		t.Fatalf("unexpected provider: %s", snap.Provider)
	}
	if snap.Bid != "1.00000000" || snap.Ask != "1.20000000" {
		t.Fatalf("unexpected bid/ask: %s / %s", snap.Bid, snap.Ask)
	}
	if snap.Mid != "1.10000000" {
		t.Fatalf("unexpected mid price: %s", snap.Mid)
	}
	if snap.SpreadBps != "1818.18181818" {
		t.Fatalf("unexpected spread bps: %s", snap.SpreadBps)
	}
}

func TestServiceUpsertPairInvertsPrices(t *testing.T) {
	store := &ratesStoreStub{}
	svc := testService(store, map[mmarket.Driver]mmarket.Connector{
		mmarket.DriverCoinGecko: &connectorStub{
			id: mmarket.DriverCoinGecko,
			ticker: &mmarket.Ticker{
				Symbol:   "RUBUSDT",
				BidPrice: "2",
				AskPrice: "4",
				Provider: "coingecko",
			},
		},
	})

	pair := config.Pair{
		PairConfig: config.PairConfig{
			Base:     "RUB",
			Quote:    "USDT",
			Symbol:   "RUBUSDT",
			Provider: "coingecko",
			Invert:   true,
		},
		Source: mmarket.DriverCoinGecko,
	}

	if err := svc.upsertPair(context.Background(), pair); err != nil {
		t.Fatalf("upsertPair returned error: %v", err)
	}

	snap := store.snapshots[0]
	if snap.Bid != "0.25000000" || snap.Ask != "0.50000000" {
		t.Fatalf("unexpected inverted bid/ask: %s / %s", snap.Bid, snap.Ask)
	}
}

func TestServicePollOnceReturnsFirstError(t *testing.T) {
	errFetch := merrors.Internal("fetch failed")
	connectorSuccess := &connectorStub{
		id: mmarket.DriverBinance,
		ticker: &mmarket.Ticker{
			Symbol:   "EURUSDT",
			BidPrice: "1",
			AskPrice: "1",
			Provider: "binance",
		},
	}
	connectorFail := &connectorStub{
		id:  mmarket.DriverCoinGecko,
		err: errFetch,
	}

	store := &ratesStoreStub{}
	svc := testService(store, map[mmarket.Driver]mmarket.Connector{
		mmarket.DriverBinance:   connectorSuccess,
		mmarket.DriverCoinGecko: connectorFail,
	})
	svc.pairs = []config.Pair{
		{PairConfig: config.PairConfig{Base: "USDT", Quote: "EUR", Symbol: "EURUSDT"}, Source: mmarket.DriverBinance},
		{PairConfig: config.PairConfig{Base: "USDT", Quote: "RUB", Symbol: "RUBUSDT"}, Source: mmarket.DriverCoinGecko},
	}

	err := svc.pollOnce(context.Background())
	if err == nil {
		t.Fatalf("pollOnce expected to return error")
	}
	if !errors.Is(err, errFetch) {
		t.Fatalf("pollOnce returned unexpected error: %v", err)
	}
	if connectorSuccess.calls != 1 {
		t.Fatalf("expected success connector called once, got %d", connectorSuccess.calls)
	}
	if connectorFail.calls != 1 {
		t.Fatalf("expected failing connector called once, got %d", connectorFail.calls)
	}
	if len(store.snapshots) != 1 {
		t.Fatalf("expected snapshot stored only for successful pair, got %d", len(store.snapshots))
	}
}

// -- test helpers -----------------------------------------------------------------

type ratesStoreStub struct {
	snapshots []*model.RateSnapshot
	err       error
}

func (r *ratesStoreStub) UpsertSnapshot(_ context.Context, snapshot *model.RateSnapshot) error {
	if r.err != nil {
		return r.err
	}
	cp := *snapshot
	r.snapshots = append(r.snapshots, &cp)
	return nil
}

func (r *ratesStoreStub) LatestSnapshot(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) {
	return nil, errNoSnapshot
}

type connectorStub struct {
	id     mmarket.Driver
	ticker *mmarket.Ticker
	err    error
	calls  int
}

func (c *connectorStub) ID() mmarket.Driver {
	return c.id
}

func (c *connectorStub) FetchTicker(_ context.Context, symbol string) (*mmarket.Ticker, error) {
	c.calls++
	if c.ticker != nil {
		cp := *c.ticker
		cp.Symbol = symbol
		return &cp, c.err
	}
	return nil, c.err
}

func testService(store storage.RatesStore, connectors map[mmarket.Driver]mmarket.Connector) *Service {
	return &Service{
		logger:     zap.NewNop(),
		cfg:        &config.Config{},
		rates:      store,
		connectors: connectors,
		pairs:      nil,
		metrics:    nil,
	}
}
