package importer

import (
	"context"
	"testing"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
)

// fakeProvider returns a fixed instrument/bar set regardless of request, and
// records the requests it receives so tests can assert selector/provider/date
// range pass-through without live provider wiring.
type fakeProvider struct {
	items    []InstrumentBars
	requests []DailyBarRequest
	err      error
}

func (p *fakeProvider) FetchDailyBars(_ context.Context, request DailyBarRequest) ([]InstrumentBars, error) {
	p.requests = append(p.requests, request)
	if p.err != nil {
		return nil, p.err
	}
	return p.items, nil
}

// barKey mirrors the bars primary key (instrument_id, timeframe, timestamp) so
// the in-memory store replaces rows the same way the PostgreSQL UpsertBar does.
type barKey struct {
	instrumentID market.InstrumentID
	timeframe    market.Timeframe
	timestamp    time.Time
}

// memStore is an in-memory InstrumentStore + BarStore for importer tests.
type memStore struct {
	instruments map[market.InstrumentID]market.Instrument
	bars        map[barKey]market.Bar
}

func newMemStore() *memStore {
	return &memStore{
		instruments: make(map[market.InstrumentID]market.Instrument),
		bars:        make(map[barKey]market.Bar),
	}
}

func (s *memStore) UpsertInstrument(_ context.Context, inst market.Instrument) error {
	s.instruments[inst.ID] = inst
	return nil
}

func (s *memStore) GetInstrument(_ context.Context, id market.InstrumentID) (market.Instrument, error) {
	return s.instruments[id], nil
}

func (s *memStore) ListInstruments(_ context.Context) ([]market.Instrument, error) {
	out := make([]market.Instrument, 0, len(s.instruments))
	for _, inst := range s.instruments {
		out = append(out, inst)
	}
	return out, nil
}

func (s *memStore) UpsertBar(_ context.Context, bar market.Bar) error {
	s.bars[barKey{bar.InstrumentID, bar.Timeframe, bar.Timestamp}] = bar
	return nil
}

func (s *memStore) GetBars(_ context.Context, id market.InstrumentID, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error) {
	out := make([]market.Bar, 0)
	for k, bar := range s.bars {
		if k.instrumentID != id || k.timeframe != timeframe {
			continue
		}
		if k.timestamp.Before(from) || k.timestamp.After(to) {
			continue
		}
		out = append(out, bar)
	}
	return out, nil
}

func sampleDailyBars() []InstrumentBars {
	seoul := time.FixedZone("Asia/Seoul", 9*60*60)
	inst := market.Instrument{
		ID:              market.InstrumentID("KRX:005930"),
		Market:          market.MarketKR,
		Venue:           market.VenueKRX,
		Symbol:          "005930",
		Name:            "Samsung Electronics",
		Currency:        market.CurrencyKRW,
		ProviderSymbols: map[string]string{string(market.ProviderKIS): "005930"},
	}
	price := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}}
	}
	bars := []market.Bar{
		{
			InstrumentID: inst.ID,
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 5, 27, 0, 0, 0, 0, seoul),
			Open:         price("74800"),
			High:         price("75600"),
			Low:          price("74400"),
			Close:        price("75000"),
			Volume:       market.Quantity{Amount: market.Decimal{Value: "9100000"}},
		},
		{
			InstrumentID: inst.ID,
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 5, 28, 0, 0, 0, 0, seoul),
			Open:         price("75200"),
			High:         price("76200"),
			Low:          price("75100"),
			Close:        price("76000"),
			Volume:       market.Quantity{Amount: market.Decimal{Value: "10500000"}},
		},
	}
	return []InstrumentBars{{Instrument: inst, Bars: bars}}
}

func sampleUSDailyBars() []InstrumentBars {
	ny := time.FixedZone("America/New_York", -4*60*60)
	inst := market.Instrument{
		ID:              market.InstrumentID("NASDAQ:AAPL"),
		Market:          market.MarketUS,
		Venue:           market.VenueNASDAQ,
		Symbol:          "AAPL",
		Name:            "Apple Inc.",
		Currency:        market.CurrencyUSD,
		AssetType:       market.AssetTypeEquity,
		ProviderSymbols: map[string]string{string(market.ProviderKIS): "AAPL"},
	}
	price := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: v}}
	}
	bars := []market.Bar{
		{
			InstrumentID: inst.ID,
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 5, 28, 0, 0, 0, 0, ny),
			Open:         price("191.51"),
			High:         price("193.00"),
			Low:          price("189.10"),
			Close:        price("189.99"),
			Volume:       market.Quantity{Amount: market.Decimal{Value: "52280051"}},
		},
		{
			InstrumentID: inst.ID,
			Timeframe:    market.TimeframeDaily,
			Timestamp:    time.Date(2024, 5, 29, 0, 0, 0, 0, ny),
			Open:         price("189.61"),
			High:         price("192.25"),
			Low:          price("189.51"),
			Close:        price("190.29"),
			Volume:       market.Quantity{Amount: market.Decimal{Value: "53068016"}},
		},
	}
	return []InstrumentBars{{Instrument: inst, Bars: bars}}
}

func usWatchlistRequest() DailyBarRequest {
	ny := time.FixedZone("America/New_York", -4*60*60)
	return DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Market:  market.MarketUS,
			Venue:   market.VenueNASDAQ,
			Symbols: []string{"AAPL"},
		},
		From: time.Date(2024, 5, 28, 0, 0, 0, 0, ny),
		To:   time.Date(2024, 5, 29, 0, 0, 0, 0, ny),
	}
}

func watchlistRequest() DailyBarRequest {
	seoul := time.FixedZone("Asia/Seoul", 9*60*60)
	return DailyBarRequest{
		Provider: market.ProviderKIS,
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorWatchlist,
			Venue:   market.VenueKRX,
			Symbols: []string{"005930"},
		},
		From: time.Date(2024, 5, 27, 0, 0, 0, 0, seoul),
		To:   time.Date(2024, 5, 28, 0, 0, 0, 0, seoul),
	}
}

func TestImporterStoresInstrumentAndBars(t *testing.T) {
	provider := &fakeProvider{items: sampleDailyBars()}
	store := newMemStore()
	imp := New(provider, store)

	req := watchlistRequest()
	result, err := imp.ImportDailyBars(context.Background(), req)
	if err != nil {
		t.Fatalf("import daily bars: %v", err)
	}

	if result.Instruments != 1 {
		t.Errorf("result instruments: got %d, want 1", result.Instruments)
	}
	if result.Bars != 2 {
		t.Errorf("result bars: got %d, want 2", result.Bars)
	}
	if len(store.instruments) != 1 {
		t.Errorf("stored instruments: got %d, want 1", len(store.instruments))
	}
	if len(store.bars) != 2 {
		t.Errorf("stored bars: got %d, want 2", len(store.bars))
	}

	// The provider must receive the full request, not just the selector.
	if len(provider.requests) != 1 {
		t.Fatalf("provider requests: got %d, want 1", len(provider.requests))
	}
	got := provider.requests[0]
	if got.Provider != req.Provider {
		t.Errorf("request provider: got %q, want %q", got.Provider, req.Provider)
	}
	if got.Selector.Kind != req.Selector.Kind || got.Selector.Venue != req.Selector.Venue {
		t.Errorf("request selector: got %+v, want %+v", got.Selector, req.Selector)
	}
	if !got.From.Equal(req.From) || !got.To.Equal(req.To) {
		t.Errorf("request date range: got [%s,%s], want [%s,%s]", got.From, got.To, req.From, req.To)
	}
}

func TestImporterStoresUSInstrumentWithAssetType(t *testing.T) {
	provider := &fakeProvider{items: sampleUSDailyBars()}
	store := newMemStore()
	imp := New(provider, store)

	result, err := imp.ImportDailyBars(context.Background(), usWatchlistRequest())
	if err != nil {
		t.Fatalf("import US daily bars: %v", err)
	}

	if result.Instruments != 1 || result.Bars != 2 {
		t.Errorf("result counts: got %d instruments / %d bars, want 1/2", result.Instruments, result.Bars)
	}

	// The US instrument must land with its USD currency and asset_type intact,
	// so the storage upsert path carries instrument metadata, not just bars.
	stored, ok := store.instruments[market.InstrumentID("NASDAQ:AAPL")]
	if !ok {
		t.Fatal("expected NASDAQ:AAPL instrument to be stored")
	}
	if stored.Market != market.MarketUS || stored.Venue != market.VenueNASDAQ {
		t.Errorf("stored market/venue: got %q/%q, want US/NASDAQ", stored.Market, stored.Venue)
	}
	if stored.Currency != market.CurrencyUSD {
		t.Errorf("stored currency: got %q, want USD", stored.Currency)
	}
	if stored.AssetType != market.AssetTypeEquity {
		t.Errorf("stored asset type: got %q, want equity", stored.AssetType)
	}
	if len(store.bars) != 2 {
		t.Errorf("stored bars: got %d, want 2", len(store.bars))
	}
}

func TestImporterIsIdempotentForSameDailyBars(t *testing.T) {
	provider := &fakeProvider{items: sampleDailyBars()}
	store := newMemStore()
	imp := New(provider, store)

	if _, err := imp.ImportDailyBars(context.Background(), watchlistRequest()); err != nil {
		t.Fatalf("first import: %v", err)
	}
	if _, err := imp.ImportDailyBars(context.Background(), watchlistRequest()); err != nil {
		t.Fatalf("second import: %v", err)
	}

	if len(provider.requests) != 2 {
		t.Errorf("provider calls: got %d, want 2", len(provider.requests))
	}
	// Re-importing the same selector must replace rows in place, not duplicate
	// them: the store keys bars by (instrument_id, timeframe, timestamp).
	if len(store.instruments) != 1 {
		t.Errorf("instruments after re-import: got %d, want 1", len(store.instruments))
	}
	if len(store.bars) != 2 {
		t.Errorf("bars after re-import: got %d, want 2", len(store.bars))
	}
}
