package aggregation

import (
	"context"
	"errors"
	"testing"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/services/worker/internal/socket"
)

// fakeBarStoreForIntegration implements storage.BarStore with configurable behavior.
type fakeBarStoreForIntegration struct {
	bars      []market.Bar
	upserted  []market.Bar
	err       error
	errByInst map[market.InstrumentID]error
}

func (f *fakeBarStoreForIntegration) UpsertBar(ctx context.Context, bar market.Bar) error {
	f.upserted = append(f.upserted, bar)
	f.bars = append(f.bars, bar)
	return nil
}

func (f *fakeBarStoreForIntegration) GetBars(ctx context.Context, id market.InstrumentID, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error) {
	if f.err != nil {
		return nil, f.err
	}
	if err := f.errByInst[id]; err != nil {
		return nil, err
	}
	bars := make([]market.Bar, 0)
	for _, bar := range f.bars {
		if bar.InstrumentID != id || bar.Timeframe != timeframe {
			continue
		}
		if !from.IsZero() && bar.Timestamp.Before(from) {
			continue
		}
		if !to.IsZero() && bar.Timestamp.After(to) {
			continue
		}
		bars = append(bars, bar)
	}
	return bars, nil
}

// fakeInstrumentStoreForIntegration implements storage.InstrumentStore.
type fakeInstrumentStoreForIntegration struct {
	insts []market.Instrument
	err   error
}

func (f *fakeInstrumentStoreForIntegration) UpsertInstrument(ctx context.Context, inst market.Instrument) error {
	return nil
}

func (f *fakeInstrumentStoreForIntegration) GetInstrument(ctx context.Context, id market.InstrumentID) (market.Instrument, error) {
	return market.Instrument{}, nil
}

func (f *fakeInstrumentStoreForIntegration) ListInstruments(ctx context.Context) ([]market.Instrument, error) {
	return f.insts, f.err
}

// makeDailyBar creates a daily bar for testing.
func makeDailyBar(instID market.InstrumentID, ts time.Time) market.Bar {
	return market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    ts,
		Open:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67000"}},
		High:         market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "68000"}},
		Low:          market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "66000"}},
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "67500"}},
		Volume:       market.Quantity{Amount: market.Decimal{Value: "25000000"}},
	}
}

func TestAggregateMonthlyBarsNoData(t *testing.T) {
	inst := market.Instrument{
		ID:              "KRX:005930",
		Symbol:          "005930",
		ProviderSymbols: map[string]string{"kis": "005930"},
	}
	bars := &fakeBarStoreForIntegration{}
	insts := &fakeInstrumentStoreForIntegration{insts: []market.Instrument{inst}}
	agg := NewAggregator(bars, insts)

	result, err := agg.AggregateMonthlyBars(context.Background(), socket.AggregateMonthlyRequest{
		Provider: "kis",
		Selector: market.UniverseSelector{Kind: market.UniverseSelectorWatchlist, Symbols: []string{"005930"}},
		From:     time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
		To:       time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
	})
	if err == nil {
		t.Fatal("expected no-data error, got nil")
	}
	if !errors.Is(err, socket.ErrMonthlySourceBarsNotFound) {
		t.Fatalf("expected ErrMonthlySourceBarsNotFound, got %v", err)
	}
	if len(result.Provenance) != 0 {
		t.Errorf("expected no provenance on no-data error, got %d", len(result.Provenance))
	}
}

func TestAggregateMonthlyBarsStorageReadError(t *testing.T) {
	first := market.Instrument{
		ID:              "KRX:005930",
		Symbol:          "005930",
		ProviderSymbols: map[string]string{"kis": "005930"},
	}
	second := market.Instrument{
		ID:              "KRX:000660",
		Symbol:          "000660",
		ProviderSymbols: map[string]string{"kis": "000660"},
	}
	boom := errors.New("storage boom")
	bars := &fakeBarStoreForIntegration{
		bars: []market.Bar{
			makeDailyBar(first.ID, time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)),
		},
		errByInst: map[market.InstrumentID]error{second.ID: boom},
	}
	insts := &fakeInstrumentStoreForIntegration{insts: []market.Instrument{first, second}}
	agg := NewAggregator(bars, insts)

	result, err := agg.AggregateMonthlyBars(context.Background(), socket.AggregateMonthlyRequest{
		Provider: "kis",
		Selector: market.UniverseSelector{Kind: market.UniverseSelectorWatchlist, Symbols: []string{"005930", "000660"}},
		From:     time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
		To:       time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
	})
	if err == nil {
		t.Fatal("expected error for storage read failure, got nil")
	}
	if !errors.Is(err, boom) {
		t.Fatalf("expected storage boom to be wrapped, got %v", err)
	}
	if result.MonthlyBarCount == 0 {
		t.Fatal("test setup should aggregate the first instrument before the second read fails")
	}
}

func TestAggregateMonthlyBarsMatchesProviderSymbolValue(t *testing.T) {
	inst := market.Instrument{
		ID:              "KRX:005930",
		Symbol:          "Samsung Electronics",
		ProviderSymbols: map[string]string{"kis": "005930"},
	}
	bars := &fakeBarStoreForIntegration{
		bars: []market.Bar{
			makeDailyBar(inst.ID, time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)),
		},
	}
	insts := &fakeInstrumentStoreForIntegration{insts: []market.Instrument{inst}}
	agg := NewAggregator(bars, insts)

	result, err := agg.AggregateMonthlyBars(context.Background(), socket.AggregateMonthlyRequest{
		Provider: "kis",
		Selector: market.UniverseSelector{Kind: market.UniverseSelectorWatchlist, Symbols: []string{"005930"}},
		From:     time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
		To:       time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Instruments != 1 {
		t.Fatalf("expected provider symbol to select one instrument, got %d", result.Instruments)
	}
}

func TestAggregateMonthlyBarsPersistedMonthlyTimeframe(t *testing.T) {
	inst := market.Instrument{
		ID:              "KRX:005930",
		Symbol:          "005930",
		ProviderSymbols: map[string]string{"kis": "005930"},
	}
	storedBars := []market.Bar{
		makeDailyBar(inst.ID, time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)),
		makeDailyBar(inst.ID, time.Date(2024, 2, 2, 0, 0, 0, 0, time.UTC)),
	}
	bars := &fakeBarStoreForIntegration{bars: storedBars}
	insts := &fakeInstrumentStoreForIntegration{insts: []market.Instrument{inst}}
	agg := NewAggregator(bars, insts)

	result, err := agg.AggregateMonthlyBars(context.Background(), socket.AggregateMonthlyRequest{
		Provider: "kis",
		Selector: market.UniverseSelector{Kind: market.UniverseSelectorWatchlist, Symbols: []string{"005930"}},
		From:     time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
		To:       time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(result.Provenance) != 2 {
		t.Fatalf("expected 2 provenance entries, got %d", len(result.Provenance))
	}
	if result.MonthlyBarCount != 2 {
		t.Errorf("expected 2 monthly bars, got %d", result.MonthlyBarCount)
	}
	if len(bars.upserted) != 2 {
		t.Fatalf("expected 2 stored monthly bars, got %d", len(bars.upserted))
	}
	for _, bar := range bars.upserted {
		if bar.Timeframe != market.TimeframeMonthly {
			t.Fatalf("upserted bar timeframe = %q, want %q", bar.Timeframe, market.TimeframeMonthly)
		}
	}

	dailyBars, err := bars.GetBars(context.Background(), inst.ID, market.TimeframeDaily,
		time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
		time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
	)
	if err != nil {
		t.Fatalf("daily read unexpected error: %v", err)
	}
	if len(dailyBars) != 2 {
		t.Fatalf("expected 2 daily bars after monthly upsert, got %d", len(dailyBars))
	}
	for _, bar := range dailyBars {
		if bar.Timeframe != market.TimeframeDaily {
			t.Fatalf("daily read returned timeframe %q", bar.Timeframe)
		}
	}

	monthlyBars, err := bars.GetBars(context.Background(), inst.ID, market.TimeframeMonthly,
		time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
		time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
	)
	if err != nil {
		t.Fatalf("monthly read unexpected error: %v", err)
	}
	if len(monthlyBars) != 2 {
		t.Fatalf("expected 2 monthly bars after upsert, got %d", len(monthlyBars))
	}
	for _, bar := range monthlyBars {
		if bar.Timeframe != market.TimeframeMonthly {
			t.Fatalf("monthly read returned timeframe %q", bar.Timeframe)
		}
	}
}
