package backtest

import (
	"context"
	"reflect"
	"sync"
	"testing"
	"time"

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

type inMemoryResultStore struct {
	mu      sync.RWMutex
	results map[backtest.RunID]backtest.Result
}

func newInMemoryResultStore() *inMemoryResultStore {
	return &inMemoryResultStore{
		results: make(map[backtest.RunID]backtest.Result),
	}
}

func (m *inMemoryResultStore) UpsertResult(ctx context.Context, result backtest.Result) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.results[result.RunID] = result
	return nil
}

func (m *inMemoryResultStore) GetResult(ctx context.Context, id backtest.RunID) (backtest.Result, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	res, ok := m.results[id]
	if !ok {
		return backtest.Result{}, storage.ErrResultNotFound
	}
	return res, nil
}

type inMemoryBarSource struct {
	bars []market.Bar
}

func (m *inMemoryBarSource) GetBars(ctx context.Context, mkt market.Market, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error) {
	return m.bars, nil
}

type inMemoryStrategyPort struct {
	strategy backtest.Strategy
}

func (m *inMemoryStrategyPort) GetStrategy(ctx context.Context, id backtest.StrategyID) (backtest.Strategy, error) {
	return m.strategy, nil
}

type deterministicStrategy struct {
	id backtest.StrategyID
}

func (s *deterministicStrategy) ID() backtest.StrategyID {
	return s.id
}

func (s *deterministicStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	// Buy 2 units of the instrument on the first bar (May 1),
	// and sell 1 unit on the second bar (May 2).
	day := input.Bar.Timestamp.Day()
	if day == 1 {
		return []backtest.OrderIntent{
			{
				InstrumentID: input.Bar.InstrumentID,
				Side:         backtest.OrderSideBuy,
				Quantity:     market.Quantity{Amount: market.Decimal{Value: "2"}},
			},
		}, nil
	} else if day == 2 {
		return []backtest.OrderIntent{
			{
				InstrumentID: input.Bar.InstrumentID,
				Side:         backtest.OrderSideSell,
				Quantity:     market.Quantity{Amount: market.Decimal{Value: "1"}},
			},
		}, nil
	}
	return nil, nil
}

func TestEngineProducesDeterministicResultFromFixtureBars(t *testing.T) {
	instID := market.InstrumentID("KRX:005930")
	bar1 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1000"}},
	}
	bar2 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1100"}},
	}
	bar3 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1200"}},
	}

	barSource := &inMemoryBarSource{bars: []market.Bar{bar3, bar1, bar2}} // unsorted
	strat := &deterministicStrategy{id: "det-strat"}
	strategyPort := &inMemoryStrategyPort{strategy: strat}
	resultStore := newInMemoryResultStore()

	engine := NewEngine(barSource, strategyPort, resultStore)

	run := backtest.Run{
		ID: "run-deterministic",
		Spec: backtest.RunSpec{
			StrategyID: "det-strat",
			Market:     market.MarketKR,
			Timeframe:  market.TimeframeDaily,
			From:       time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
			To:         time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
		},
	}

	// First execution
	err := engine.Execute(context.Background(), run)
	if err != nil {
		t.Fatalf("first execution failed: %v", err)
	}
	res1, err := resultStore.GetResult(context.Background(), run.ID)
	if err != nil {
		t.Fatalf("failed to get first result: %v", err)
	}

	// Clear the store to make sure upsert actually writes the second result
	resultStore.mu.Lock()
	delete(resultStore.results, run.ID)
	resultStore.mu.Unlock()

	// Second execution
	err = engine.Execute(context.Background(), run)
	if err != nil {
		t.Fatalf("second execution failed: %v", err)
	}
	res2, err := resultStore.GetResult(context.Background(), run.ID)
	if err != nil {
		t.Fatalf("failed to get second result: %v", err)
	}

	// Verify deep equality
	if !reflect.DeepEqual(res1, res2) {
		t.Errorf("results are not identical.\nResult 1: %+v\nResult 2: %+v", res1, res2)
	}
}

func TestEngineRunWithUSMarketReturnsEquityInUSD(t *testing.T) {
	// This smoke test validates the MarketUS path: USD starting cash,
	// USD-denominated prices, and a final equity figure expressed in USD.
	// The strategy is the same deterministic one: buy 2 on day 1, sell 1 on day 2.
	//
	// Starting cash = 10,000 USD
	// Day 1: Buy 2 units at 100 USD → cash = 9,800, pos = 2
	// Day 2: Sell 1 unit at 110 USD → cash = 9,910, pos = 1
	// Day 3: No trades; pos marked at 120 USD
	// Final equity = cash 9,910 + 1*120 = 10,030 USD
	//
	// Trades: buy 2@100, sell 1@110
	// Positions: 1 unit NASDAQ:AAPL at last price 120 USD
	// Total return = (10030 - 10000) / 10000 = 0.003

	instID := market.InstrumentID("NASDAQ:AAPL")
	bar1 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "100"}},
	}
	bar2 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "110"}},
	}
	bar3 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyUSD, Amount: market.Decimal{Value: "120"}},
	}

	barSource := &inMemoryBarSource{bars: []market.Bar{bar1, bar2, bar3}}
	strat := &deterministicStrategy{id: "det-strat-us"}
	strategyPort := &inMemoryStrategyPort{strategy: strat}
	resultStore := newInMemoryResultStore()

	engine := NewEngine(barSource, strategyPort, resultStore)

	run := backtest.Run{
		ID: "run-us-smoke",
		Spec: backtest.RunSpec{
			StrategyID: "det-strat-us",
			Market:     market.MarketUS,
			Timeframe:  market.TimeframeDaily,
			From:       time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
			To:         time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
		},
	}

	err := engine.Execute(context.Background(), run)
	if err != nil {
		t.Fatalf("US execution failed: %v", err)
	}

	res, err := resultStore.GetResult(context.Background(), run.ID)
	if err != nil {
		t.Fatalf("failed to get result: %v", err)
	}

	// Verify starting cash is USD 10,000
	if res.StartingCash.Currency != market.CurrencyUSD {
		t.Errorf("starting cash currency: got %q, want %q", res.StartingCash.Currency, market.CurrencyUSD)
	}
	if res.StartingCash.Amount.Value != "10000" {
		t.Errorf("expected starting cash 10000, got %s", res.StartingCash.Amount.Value)
	}

	// Verify final equity
	// Day 1: cash 9,800 + 2*100 = 10,000
	// Day 2: cash 9,910 + 1*110 = 10,020
	// Day 3: cash 9,910 + 1*120 = 10,030
	if res.EndingEquity.Currency != market.CurrencyUSD {
		t.Errorf("ending equity currency: got %q, want %q", res.EndingEquity.Currency, market.CurrencyUSD)
	}
	if res.EndingEquity.Amount.Value != "10030" {
		t.Errorf("expected ending equity 10030, got %s", res.EndingEquity.Amount.Value)
	}

	// Verify Trades
	if len(res.Trades) != 2 {
		t.Fatalf("expected 2 trades, got %d", len(res.Trades))
	}
	trade1 := res.Trades[0]
	if trade1.Side != backtest.OrderSideBuy || trade1.Quantity.Amount.Value != "2" ||
		trade1.Price.Amount.Value != "100" {
		t.Errorf("unexpected trade 1: %+v", trade1)
	}
	trade2 := res.Trades[1]
	if trade2.Side != backtest.OrderSideSell || trade2.Quantity.Amount.Value != "1" ||
		trade2.Price.Amount.Value != "110" {
		t.Errorf("unexpected trade 2: %+v", trade2)
	}

	// Verify Positions
	if len(res.Positions) != 1 {
		t.Fatalf("expected 1 position, got %d", len(res.Positions))
	}
	pos := res.Positions[0]
	if pos.InstrumentID != instID || pos.Quantity.Amount.Value != "1" ||
		pos.LastPrice.Amount.Value != "120" {
		t.Errorf("unexpected position: %+v", pos)
	}

	// Verify derived summary metrics
	if res.Summary.EndingEquity.Amount.Value != "10030" {
		t.Errorf("summary ending equity: got %s, want 10030", res.Summary.EndingEquity.Amount.Value)
	}
	if res.Summary.TradeCount != 2 {
		t.Errorf("summary trade count: got %d, want 2", res.Summary.TradeCount)
	}
	// (10030 - 10000) / 10000 = 0.003
	if res.Summary.TotalReturn.Value != "0.003" {
		t.Errorf("summary total return: got %s, want 0.003", res.Summary.TotalReturn.Value)
	}

	// Verify equity curve: one point per bar
	if len(res.EquityCurve) != 3 {
		t.Fatalf("equity curve length: got %d, want 3", len(res.EquityCurve))
	}
	wantCurve := []struct {
		ts     time.Time
		equity string
	}{
		{bar1.Timestamp, "10000"},
		{bar2.Timestamp, "10020"},
		{bar3.Timestamp, "10030"},
	}
	for i, want := range wantCurve {
		point := res.EquityCurve[i]
		if !point.Timestamp.Equal(want.ts) {
			t.Errorf("equity curve point %d timestamp: got %v, want %v", i, point.Timestamp, want.ts)
		}
		if point.Equity.Amount.Value != want.equity {
			t.Errorf("equity curve point %d equity: got %s, want %s", i, point.Equity.Amount.Value, want.equity)
		}
		if point.Equity.Currency != market.CurrencyUSD {
			t.Errorf("equity curve point %d currency: got %q, want %q", i, point.Equity.Currency, market.CurrencyUSD)
		}
	}

	// Final equity curve point must match ending equity
	finalPoint := res.EquityCurve[len(res.EquityCurve)-1]
	if finalPoint.Equity.Amount.Value != res.EndingEquity.Amount.Value {
		t.Errorf("final equity point %s does not match ending equity %s",
			finalPoint.Equity.Amount.Value, res.EndingEquity.Amount.Value)
	}
}

func TestEngineStoresAndQueriesFixtureResult(t *testing.T) {
	instID := market.InstrumentID("KRX:005930")
	bar1 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1000"}},
	}
	bar2 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1100"}},
	}
	bar3 := market.Bar{
		InstrumentID: instID,
		Timeframe:    market.TimeframeDaily,
		Timestamp:    time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
		Close:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1200"}},
	}

	barSource := &inMemoryBarSource{bars: []market.Bar{bar1, bar2, bar3}}
	strat := &deterministicStrategy{id: "det-strat"}
	strategyPort := &inMemoryStrategyPort{strategy: strat}
	resultStore := newInMemoryResultStore()

	engine := NewEngine(barSource, strategyPort, resultStore)

	run := backtest.Run{
		ID: "run-query",
		Spec: backtest.RunSpec{
			StrategyID: "det-strat",
			Market:     market.MarketKR,
			Timeframe:  market.TimeframeDaily,
			From:       time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
			To:         time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
		},
	}

	err := engine.Execute(context.Background(), run)
	if err != nil {
		t.Fatalf("execution failed: %v", err)
	}

	res, err := resultStore.GetResult(context.Background(), run.ID)
	if err != nil {
		t.Fatalf("failed to get result: %v", err)
	}

	// Let's manually calculate the expected values:
	// Starting cash = 10,000,000 KRW
	//
	// Day 1: Buy 2 units at 1000 KRW
	// Cash = 10,000,000 - 2,000 = 9,998,000 KRW
	// Position = 2 units, mark price = 1000 KRW
	//
	// Day 2: Sell 1 unit at 1100 KRW
	// Cash = 9,998,000 + 1,100 = 9,999,100 KRW
	// Position = 1 unit, mark price = 1100 KRW
	//
	// Day 3: No trades.
	// Position = 1 unit, marked at last price = 1200 KRW
	// Final Equity = Cash (9,999,100) + 1 * 1200 = 10,000,300 KRW
	//
	// Expected trades:
	// 1. Buy 2 units at 1000 KRW on May 1
	// 2. Sell 1 unit at 1100 KRW on May 2
	//
	// Expected positions:
	// 1. 1 unit of KRX:005930 at last price 1200 KRW

	if res.StartingCash.Amount.Value != "10000000" {
		t.Errorf("expected starting cash 10000000, got %s", res.StartingCash.Amount.Value)
	}
	if res.EndingEquity.Amount.Value != "10000300" {
		t.Errorf("expected ending equity 10000300, got %s", res.EndingEquity.Amount.Value)
	}

	// Verify Trades
	if len(res.Trades) != 2 {
		t.Fatalf("expected 2 trades, got %d", len(res.Trades))
	}
	trade1 := res.Trades[0]
	if trade1.Side != backtest.OrderSideBuy || trade1.Quantity.Amount.Value != "2" || trade1.Price.Amount.Value != "1000" {
		t.Errorf("unexpected trade 1: %+v", trade1)
	}
	trade2 := res.Trades[1]
	if trade2.Side != backtest.OrderSideSell || trade2.Quantity.Amount.Value != "1" || trade2.Price.Amount.Value != "1100" {
		t.Errorf("unexpected trade 2: %+v", trade2)
	}

	// Verify Positions
	if len(res.Positions) != 1 {
		t.Fatalf("expected 1 position, got %d", len(res.Positions))
	}
	pos := res.Positions[0]
	if pos.InstrumentID != instID || pos.Quantity.Amount.Value != "1" || pos.LastPrice.Amount.Value != "1200" {
		t.Errorf("unexpected position: %+v", pos)
	}

	// Verify derived summary metrics.
	if res.Summary.StartingCash.Amount.Value != "10000000" {
		t.Errorf("summary starting cash: got %s, want 10000000", res.Summary.StartingCash.Amount.Value)
	}
	if res.Summary.EndingEquity.Amount.Value != "10000300" {
		t.Errorf("summary ending equity: got %s, want 10000300", res.Summary.EndingEquity.Amount.Value)
	}
	if res.Summary.TradeCount != 2 {
		t.Errorf("summary trade count: got %d, want 2", res.Summary.TradeCount)
	}
	// (10000300 - 10000000) / 10000000 = 0.00003
	if res.Summary.TotalReturn.Value != "0.00003" {
		t.Errorf("summary total return: got %s, want 0.00003", res.Summary.TotalReturn.Value)
	}

	// Verify deterministic equity curve: one point per bar, ordered by timestamp.
	// Day 1: cash 9,998,000 + 2*1000 = 10,000,000
	// Day 2: cash 9,999,100 + 1*1100 = 10,000,200
	// Day 3: cash 9,999,100 + 1*1200 = 10,000,300
	if len(res.EquityCurve) != 3 {
		t.Fatalf("equity curve length: got %d, want 3", len(res.EquityCurve))
	}
	wantCurve := []struct {
		ts     time.Time
		equity string
	}{
		{bar1.Timestamp, "10000000"},
		{bar2.Timestamp, "10000200"},
		{bar3.Timestamp, "10000300"},
	}
	for i, want := range wantCurve {
		point := res.EquityCurve[i]
		if !point.Timestamp.Equal(want.ts) {
			t.Errorf("equity curve point %d timestamp: got %v, want %v", i, point.Timestamp, want.ts)
		}
		if point.Equity.Amount.Value != want.equity {
			t.Errorf("equity curve point %d equity: got %s, want %s", i, point.Equity.Amount.Value, want.equity)
		}
	}
	// Final equity curve point must match the reported ending equity.
	finalPoint := res.EquityCurve[len(res.EquityCurve)-1]
	if finalPoint.Equity.Amount.Value != res.EndingEquity.Amount.Value {
		t.Errorf("final equity point %s does not match ending equity %s", finalPoint.Equity.Amount.Value, res.EndingEquity.Amount.Value)
	}
}
