package papertrading

import (
	"context"
	"testing"
	"time"

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

// --- helpers ---

type testBarSource struct {
	bars []market.Bar
}

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

type testStrategyPort struct {
	strategy backtest.Strategy
}

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

// simpleBuyStrategy buys 1 unit on the first bar only.
type simpleBuyStrategy struct {
	id backtest.StrategyID
}

func (s simpleBuyStrategy) ID() backtest.StrategyID { return s.id }
func (s simpleBuyStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	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: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// buyThenSellStrategy buys on bar 1, sells 1 unit on bar 2.
type buyThenSellStrategy struct {
	id backtest.StrategyID
}

func (s buyThenSellStrategy) ID() backtest.StrategyID { return s.id }
func (s buyThenSellStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	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: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	if day == 2 {
		return []backtest.OrderIntent{{
			InstrumentID: input.Bar.InstrumentID,
			Side:         backtest.OrderSideSell,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// emptyInstOrderStrategy returns an order with empty InstrumentID to verify rejection policy.
type emptyInstOrderStrategy struct {
	id backtest.StrategyID
}

func (s emptyInstOrderStrategy) ID() backtest.StrategyID { return s.id }
func (s emptyInstOrderStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	day := input.Bar.Timestamp.Day()
	if day == 1 {
		return []backtest.OrderIntent{{
			InstrumentID: "", // deliberately empty
			Side:         backtest.OrderSideBuy,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// limitBuyStrategy: on day 1 places a limit buy at price 1050, then buys more on day 3.
type limitBuyStrategy struct {
	id backtest.StrategyID
}

func (s limitBuyStrategy) ID() backtest.StrategyID { return s.id }
func (s limitBuyStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	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: "1"}},
			Type:         backtest.OrderTypeLimit,
			LimitPrice:   market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1050"}},
		}}, nil
	}
	if day == 3 {
		return []backtest.OrderIntent{{
			InstrumentID: input.Bar.InstrumentID,
			Side:         backtest.OrderSideBuy,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "2"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// nonCrossedLimitOnSellStrategy buys 1 unit on day 1, then places a limit sell at 2000 on day 2.
// Bar highs: day1=1100, day2=1200, day3=1300. None of them cross 2000, so the order persists pending.
type nonCrossedLimitOnSellStrategy struct {
	id backtest.StrategyID
}

func (s nonCrossedLimitOnSellStrategy) ID() backtest.StrategyID { return s.id }
func (s nonCrossedLimitOnSellStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	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: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	if day == 2 {
		return []backtest.OrderIntent{{
			InstrumentID: input.Bar.InstrumentID,
			Side:         backtest.OrderSideSell,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "1"}},
			Type:         backtest.OrderTypeLimit,
			LimitPrice:   market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "2000"}},
		}}, nil
	}
	return nil, nil
}

func makeBars(tb testing.TB) []market.Bar {
	inst := market.InstrumentID("KRX:005930")
	marketKRW := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}}
	}
	return []market.Bar{
		{InstrumentID: inst, Timeframe: market.TimeframeDaily, Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC), Open: marketKRW("1000"), High: marketKRW("1100"), Low: marketKRW("950"), Close: marketKRW("1050"), Volume: market.Quantity{Amount: market.Decimal{Value: "1000"}}},
		{InstrumentID: inst, Timeframe: market.TimeframeDaily, Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC), Open: marketKRW("1050"), High: marketKRW("1200"), Low: marketKRW("1000"), Close: marketKRW("1150"), Volume: market.Quantity{Amount: market.Decimal{Value: "1200"}}},
		{InstrumentID: inst, Timeframe: market.TimeframeDaily, Timestamp: time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC), Open: marketKRW("1150"), High: marketKRW("1300"), Low: marketKRW("1100"), Close: marketKRW("1200"), Volume: market.Quantity{Amount: market.Decimal{Value: "1500"}}},
	}
}

// --- tests ---

func TestEngineFillsMarketOrderOnNextBarOpen(t *testing.T) {
	bars := makeBars(t)

	strategyPort := &testStrategyPort{strategy: simpleBuyStrategy{id: "test-simple-buy"}}
	engine := NewEngine(&testBarSource{bars: bars}, strategyPort)

	startingCash := market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID:           "paper-1",
			FillPolicy:   backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(startingCash),
		},
		Run: backtest.Run{
			ID: "run-1",
			Spec: backtest.RunSpec{
				StrategyID: "test-simple-buy",
				Market:     market.MarketKR,
				Timeframe:  market.TimeframeDaily,
				From:       bars[0].Timestamp,
				To:         bars[len(bars)-1].Timestamp,
			},
		},
	}

	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	// Strategy fires a market buy on bar 1; it should fill on bar 2 at bar 2's open price (1050).
	if len(snap.Fills) != 1 {
		t.Fatalf("expected 1 fill, got %d", len(snap.Fills))
	}
	fill := snap.Fills[0]
	if fill.Side != backtest.OrderSideBuy {
		t.Errorf("expected fill side buy, got %s", fill.Side)
	}
	// Market order fills at next-bar open
	if fill.Price.Amount.Value != "1050" {
		t.Errorf("expected fill price 1050 (bar 2 open), got %s", fill.Price.Amount.Value)
	}
	// Cash after buy: 10000000 - 1 * 1050 = 9998950
	if snap.Account.Portfolio.Cash.Amount.Value != "9998950" {
		t.Errorf("expected cash 9998950, got %s", snap.Account.Portfolio.Cash.Amount.Value)
	}
	// Position: 1 unit of KRX:005930
	pos, ok := snap.Account.Portfolio.Position(instrumentIDFromBars(t, bars))
	if !ok {
		t.Fatal("expected position after fill")
	}
	if pos.Quantity.Amount.Value != "1" {
		t.Errorf("expected position qty 1, got %s", pos.Quantity.Amount.Value)
	}
}

func TestEngineFillsLimitOrderOnDailyCrossing(t *testing.T) {
	// Bars: day1 O1000 H1100 L950 C1050, day3 O1150 H1300 L1100 C1200
	// Strategy places limit buy at 1050 on day 1.
	// Day 2 does not exist in the strategy (limitBuyStrategy orders on day 1 and 3 only) but the engine
	// iterates through all bars. The pending limit buy sits until day 2 bar high (1200) crosses 1050 (sell side).
	// For buy limit, we check: limit must be within [low, high]. For buy limit at 1050,
	// day 2: low=1000, high=1200. 1050 is between 1000 and 1200, so it fills.
	// Actually, FillOrderOnDailyBar doesn't distinguish buy/sell for limit crossing:
	// it just checks if limit is within [low, high]. 1050 is within [1000, 1200].
	// So it fills on bar 2 (day 2) at limit price 1050.

	bars := makeBars(t)

	strategyPort := &testStrategyPort{strategy: limitBuyStrategy{id: "test-limit-buy"}}
	engine := NewEngine(&testBarSource{bars: bars}, strategyPort)

	startingCash := market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID:           "paper-2",
			FillPolicy:   backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(startingCash),
		},
		Run: backtest.Run{
			ID: "run-2",
			Spec: backtest.RunSpec{
				StrategyID: "test-limit-buy",
				Market:     market.MarketKR,
				Timeframe:  market.TimeframeDaily,
				From:       bars[0].Timestamp,
				To:         bars[2].Timestamp,
			},
		},
	}

	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	// The limit buy at 1050 should fill on bar 2 (day 2) which has low=1000, high=1200
	// 1050 is between 1000 and 1200, so it crosses.
	// There's also a market buy on day 3 (bar 3) which fills on bar 4 which doesn't exist.
	if len(snap.Fills) < 1 {
		t.Fatalf("expected at least 1 fill, got %d", len(snap.Fills))
	}

	// First fill should be the limit buy
	limitFill := snap.Fills[0]
	if limitFill.Price.Amount.Value != "1050" {
		t.Errorf("expected limit fill price 1050, got %s", limitFill.Price.Amount.Value)
	}
}

func TestEngineLeavesNonCrossedLimitPending(t *testing.T) {
	// Bars day1: H1100, day2: H1200, day3: H1300.
	// Strategy buys 1 unit on day 1 (market). Buy fills on day 2 at open 1050.
	// Strategy places limit sell at 2000 on day 2. Day 2 high=1200, so limit 2000 does not cross.
	// Day 3 high=1300, limit 2000 still does not cross.
	// Result: order remains pending, 0 fills for the sell, and 0 rejections.

	bars := makeBars(t)

	strategyPort := &testStrategyPort{strategy: nonCrossedLimitOnSellStrategy{id: "test-non-crossed"}}
	engine := NewEngine(&testBarSource{bars: bars}, strategyPort)

	startingCash := market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID:           "paper-3",
			FillPolicy:   backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(startingCash),
		},
		Run: backtest.Run{
			ID: "run-3",
			Spec: backtest.RunSpec{
				StrategyID: "test-non-crossed",
				Market:     market.MarketKR,
				Timeframe:  market.TimeframeDaily,
				From:       bars[0].Timestamp,
				To:         bars[2].Timestamp,
			},
		},
	}

	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	// The market buy from day 1 fills on day 2 (open 1050) - 1 fill.
	// The limit sell from day 2 at 2000 never crosses bar high (max 1300) - does not fill.
	// Total fills = 1 (just the market buy).
	if len(snap.Fills) != 1 {
		t.Fatalf("expected 1 fill (market buy), got %d", len(snap.Fills))
	}
	fill := snap.Fills[0]
	if fill.Side != backtest.OrderSideBuy || fill.Price.Amount.Value != "1050" {
		t.Errorf("unexpected fill: %+v", fill)
	}
	// No rejections: risk check passed (we have 1 unit position), just limit didn't cross.
	if len(snap.Rejected) != 0 {
		t.Errorf("expected 0 rejections, got %d", len(snap.Rejected))
	}
	// Position still holds 1 unit (sell limit didn't fill).
	pos, ok := snap.Account.Portfolio.Position(instrumentIDFromBars(t, bars))
	if !ok {
		t.Fatal("expected position to still exist (limit sell didn't fill)")
	}
	if pos.Quantity.Amount.Value != "1" {
		t.Errorf("expected position qty 1, got %s", pos.Quantity.Amount.Value)
	}
}

func TestEngineRejectsRiskDeniedOrder(t *testing.T) {
	bars := makeBars(t)

	// Strategy tries to sell 2 units on day 1 but has no position.
	strategyPort := &testStrategyPort{strategy: riskSellStrategy{id: "test-risk-sell"}}
	engine := NewEngine(&testBarSource{bars: bars}, strategyPort)

	startingCash := market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID:           "paper-4",
			FillPolicy:   backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(startingCash),
		},
		Run: backtest.Run{
			ID: "run-4",
			Spec: backtest.RunSpec{
				StrategyID: "test-risk-sell",
				Market:     market.MarketKR,
				Timeframe:  market.TimeframeDaily,
				From:       bars[0].Timestamp,
				To:         bars[2].Timestamp,
			},
		},
	}

	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	// The sell order should be rejected because we have no position
	if len(snap.Rejected) != 1 {
		t.Fatalf("expected 1 rejected order, got %d", len(snap.Rejected))
	}
	rej := snap.Rejected[0]
	if rej.Reason == "" {
		t.Error("expected non-empty rejection reason")
	}
}

func TestEngineRecordsEquitySnapshots(t *testing.T) {
	bars := makeBars(t)

	// buyThenSell buys 1 unit on bar 1 (day 1), sells 1 unit on bar 2 (day 2).
	// Expected:
	// Bar 1 (day 1): strategy fires buy on bar 1, but fills on bar 2. No fill on bar 1.
	// Bar 2 (day 2): pending buy from bar 1 fills at bar 2 open (1050). Strategy fires sell on bar 2, fills on bar 3.
	// Bar 3 (day 3): pending sell from bar 2 fills at bar 3 open (1150). Strategy fires no order.
	// Equity at day 1: 10000000 (no position filled yet, cash unchanged)
	// Equity at day 2 cash after: 10000000 - 1050 = 9998950. position 1@1050. equity = 9998950 + 1050 = 10000000
	// But wait... bar 2 is also when the strategy sells. The sell fills on bar 3.
	// After bar 2 fill: position 1 unit at 1050. Marked at close 1200. equity = 9998950 + 1200 = 10000150
	// Bar 3: sell fills at 1150. Cash = 9998950 + 1150 = 10000100. Position = 0. equity = 10000100
	//
	// Actually let me trace more carefully:
	// Bar 1: no pending orders -> no fills. Strategy fires buy(1 unit, market). pendingOrders = [buy]. Mark price? No position yet. Equity = 10000000.
	// Bar 2: fill pending buy at bar 2 open 1050. Cash = 10000000 - 1050 = 9998950. position = 1@1050. Strategy fires sell(1 unit, market). pendingOrders = [sell]. Mark price: position exists, mark at close 1150. Equity = 9998950 + 1150 = 10000100. (Wait, but bar 2 close = 1150)
	// Bar 3: fill pending sell at bar 3 open 1150. Cash = 9998950 + 1150 = 10000100. position = 0. Strategy fires no order. No position to mark. Equity = 10000100.

	strategyPort := &testStrategyPort{strategy: buyThenSellStrategy{id: "test-buy-sell"}}
	engine := NewEngine(&testBarSource{bars: bars}, strategyPort)

	startingCash := market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID:           "paper-5",
			FillPolicy:   backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(startingCash),
		},
		Run: backtest.Run{
			ID: "run-5",
			Spec: backtest.RunSpec{
				StrategyID: "test-buy-sell",
				Market:     market.MarketKR,
				Timeframe:  market.TimeframeDaily,
				From:       bars[0].Timestamp,
				To:         bars[2].Timestamp,
			},
		},
	}

	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	if len(snap.EquityCurve) != 3 {
		t.Fatalf("expected 3 equity points, got %d", len(snap.EquityCurve))
	}

	// Bar 1: no position filled yet (strategy buy fires on bar 1 but fills on bar 2)
	if snap.EquityCurve[0].Equity.Amount.Value != "10000000" {
		t.Errorf("equity at bar 1: expected 10000000, got %s", snap.EquityCurve[0].Equity.Amount.Value)
	}

	// Bar 2: buy filled at 1050. position 1 unit. Mark at close 1150.
	// equity = 9998950 + 1150 = 10000100
	wantEq2 := "10000100"
	if snap.EquityCurve[1].Equity.Amount.Value != wantEq2 {
		t.Errorf("equity at bar 2: expected %s, got %s", wantEq2, snap.EquityCurve[1].Equity.Amount.Value)
	}

	// Bar 3: sell filled at 1150. No position. equity = 10000100
	wantEq3 := "10000100"
	if snap.EquityCurve[2].Equity.Amount.Value != wantEq3 {
		t.Errorf("equity at bar 3: expected %s, got %s", wantEq3, snap.EquityCurve[2].Equity.Amount.Value)
	}

	// Should have exactly 2 fills
	if len(snap.Fills) != 2 {
		t.Errorf("expected 2 fills, got %d", len(snap.Fills))
	}
}

// --- auxiliary ---

// riskSellStrategy places a sell order on the first bar without any position.
type riskSellStrategy struct {
	id backtest.StrategyID
}

func (s riskSellStrategy) ID() backtest.StrategyID { return s.id }
func (s riskSellStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	day := input.Bar.Timestamp.Day()
	if day == 1 {
		return []backtest.OrderIntent{{
			InstrumentID: input.Bar.InstrumentID,
			Side:         backtest.OrderSideSell,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "2"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

func instrumentIDFromBars(tb testing.TB, bars []market.Bar) market.InstrumentID {
	if len(bars) == 0 {
		tb.Fatal("empty bars")
	}
	return bars[0].InstrumentID
}

// multiInstBuyStrategy buys on the first bar of each instrument it sees.
type multiInstBuyStrategy struct {
	id backtest.StrategyID
}

func (s multiInstBuyStrategy) ID() backtest.StrategyID { return s.id }
func (s multiInstBuyStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	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: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// TestEngineMultiInstrumentPendingIsScoped verifies that a pending order for
// instrument A cannot be filled by a bar belonging to instrument B on the same
// timestamp.
func TestEngineMultiInstrumentPendingIsScoped(t *testing.T) {
	marketKRW := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}}
	}
	instA := market.InstrumentID("KRX:000660")
	instB := market.InstrumentID("KRX:005930")
	// Both on same timestamp. Strategy fires buy on day 1 for both instruments.
	// After sorting by timestamp then instrument ID: instA, instB, instA day2, instB day2
	// instA day1 → pendingA=[buyA]. instB day1 → pendingB=[buyB].
	// instA day2 → pendingA buyA fills at instA.day2.open=1000.
	// instB day2 → pendingB buyB fills at instB.day2.open=900.
	day1A := market.Bar{
		InstrumentID: instA, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("1000"), High: marketKRW("1100"), Low: marketKRW("950"), Close: marketKRW("1050"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "1000"}},
	}
	day1B := market.Bar{
		InstrumentID: instB, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("900"), High: marketKRW("950"), Low: marketKRW("850"), Close: marketKRW("920"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "800"}},
	}
	// instA day2 must NOT be consumed by instB pending
	day2A := market.Bar{
		InstrumentID: instA, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("1100"), High: marketKRW("1200"), Low: marketKRW("1050"), Close: marketKRW("1150"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "1200"}},
	}
	day2B := market.Bar{
		InstrumentID: instB, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("2000"), High: marketKRW("2100"), Low: marketKRW("1900"), Close: marketKRW("2050"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "900"}},
	}
	bars := []market.Bar{day1A, day1B, day2A, day2B}
	engine := NewEngine(&testBarSource{bars: bars}, &testStrategyPort{strategy: multiInstBuyStrategy{id: "test-multi"}})
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID: "paper-multi", FillPolicy: backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}),
		},
		Run: backtest.Run{
			ID: "run-multi",
			Spec: backtest.RunSpec{
				StrategyID: "test-multi", Market: market.MarketKR, Timeframe: market.TimeframeDaily,
				From: day1A.Timestamp, To: day2B.Timestamp,
			},
		},
	}
	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}
	if len(snap.Fills) != 2 {
		t.Fatalf("expected 2 fills, got %d", len(snap.Fills))
	}
	// Fills occur on the NEXT bar of the same instrument at that bar's open.
	// pendingA buys on day1A, fills on day2A.open=1100.
	// pendingB buys on day1B, fills on day2B.open=2000.
	if snap.Fills[0].InstrumentID != instA || snap.Fills[0].Price.Amount.Value != "1100" {
		t.Errorf("expected A fill at 1100 (day2A open), got %+v", snap.Fills[0])
	}
	if snap.Fills[1].InstrumentID != instB || snap.Fills[1].Price.Amount.Value != "2000" {
		t.Errorf("expected B fill at 2000 (day2B open), got %+v", snap.Fills[1])
	}
}

// TestEngineApplyFillFailureIsRejected verifies that a market buy that passes
// CheckRisk but fails ApplyFill (insufficient cash) ends up as RejectedOrder
// and is NOT retried on future bars.
func TestEngineApplyFillFailureIsRejected(t *testing.T) {
	marketKRW := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}}
	}
	inst := market.InstrumentID("KRX:005930")
	bars := []market.Bar{
		{InstrumentID: inst, Timeframe: market.TimeframeDaily,
			Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
			Open:      marketKRW("1000"), High: marketKRW("1100"), Low: marketKRW("950"), Close: marketKRW("1050"),
			Volume: market.Quantity{Amount: market.Decimal{Value: "1000"}},
		},
		{InstrumentID: inst, Timeframe: market.TimeframeDaily,
			Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
			Open:      marketKRW("1050"), High: marketKRW("1200"), Low: marketKRW("1000"), Close: marketKRW("1150"),
			Volume: market.Quantity{Amount: market.Decimal{Value: "1200"}},
		},
		{InstrumentID: inst, Timeframe: market.TimeframeDaily,
			Timestamp: time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC),
			Open:      marketKRW("1150"), High: marketKRW("1300"), Low: marketKRW("1100"), Close: marketKRW("1200"),
			Volume: market.Quantity{Amount: market.Decimal{Value: "1500"}},
		},
	}
	engine := NewEngine(&testBarSource{bars: bars}, &testStrategyPort{strategy: &buyLargeStrategy{id: "test-buy-large"}})
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID: "paper-reject", FillPolicy: backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "500"}}),
		},
		Run: backtest.Run{
			ID: "run-reject",
			Spec: backtest.RunSpec{
				StrategyID: "test-buy-large", Market: market.MarketKR, Timeframe: market.TimeframeDaily,
				From: bars[0].Timestamp, To: bars[2].Timestamp,
			},
		},
	}
	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}
	if len(snap.Rejected) != 1 {
		t.Fatalf("expected 1 rejected, got %d", len(snap.Rejected))
	}
	if snap.Rejected[0].Reason == "" {
		t.Error("expected non-empty rejection reason")
	}
	if len(snap.Fills) != 0 {
		t.Errorf("expected 0 fills, got %d", len(snap.Fills))
	}
	if snap.Account.Portfolio.Cash.Amount.Value != "500" {
		t.Errorf("expected cash 500 unchanged, got %s", snap.Account.Portfolio.Cash.Amount.Value)
	}
}

// buyLargeStrategy returns a buy order for 9999999 units only on the first bar (will fail ApplyFill).
type buyLargeStrategy struct {
	id backtest.StrategyID
}

func (s buyLargeStrategy) ID() backtest.StrategyID { return s.id }
func (s buyLargeStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	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: "9999999"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// orderABStrategy returns an order for instrument B while processing
// the first bar of instrument A.
type orderABStrategy struct {
	id    backtest.StrategyID
	aInst market.InstrumentID
	bInst market.InstrumentID
}

func (s orderABStrategy) ID() backtest.StrategyID { return s.id }
func (s orderABStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
	day := input.Bar.Timestamp.Day()
	if day == 1 && input.Bar.InstrumentID == s.aInst {
		return []backtest.OrderIntent{{
			InstrumentID: s.bInst,
			Side:         backtest.OrderSideBuy,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "1"}},
			Type:         backtest.OrderTypeMarket,
		}}, nil
	}
	return nil, nil
}

// TestEngineCrossInstrumentPendingKeyed verifies that a strategy can return an
// order for a different instrument than the current bar, and that order fills
// only on the matching instrument's next bar.
func TestEngineCrossInstrumentPendingKeyed(t *testing.T) {
	marketKRW := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}}
	}
	instA := market.InstrumentID("KRX:000660")
	instB := market.InstrumentID("KRX:005930")

	// Two bars with different timestamps: instA on day1, instB on day2.
	// A bar day1 → strategy fires B-order, stored in pendingOrders[B].
	// B bar day2 → Phase 1 picks up B-order, fills at day2B.open=950.
	// Key invariant: B-order stored during A bar fills at B price, not A price.
	day1A := market.Bar{
		InstrumentID: instA, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("1000"), High: marketKRW("1100"), Low: marketKRW("950"), Close: marketKRW("1050"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "1000"}},
	}
	day2B := market.Bar{
		InstrumentID: instB, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("950"), High: marketKRW("1000"), Low: marketKRW("900"), Close: marketKRW("970"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "900"}},
	}
	bars := []market.Bar{day1A, day2B}

	// Strategy returns a B-order on day 1 (while the bar is for instrument A)
	engine := NewEngine(&testBarSource{bars: bars}, &testStrategyPort{strategy: orderABStrategy{id: "test-cross-inst", aInst: instA, bInst: instB}})
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID: "paper-cross", FillPolicy: backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}),
		},
		Run: backtest.Run{
			ID: "run-cross",
			Spec: backtest.RunSpec{
				StrategyID: "test-cross-inst", Market: market.MarketKR, Timeframe: market.TimeframeDaily,
				From: day1A.Timestamp, To: day2B.Timestamp,
			},
		},
	}
	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	// The B-order fires during A's day1 processing. It sits in pendingOrders[instB]
	// and only fills when the next instB bar arrives on day2. The fill price is
	// day2B.open=950. A's bars never consume orders for B.
	if len(snap.Fills) != 1 {
		t.Fatalf("expected 1 fill from cross-instrument order, got %d", len(snap.Fills))
	}
	fill := snap.Fills[0]
	if fill.InstrumentID != instB {
		t.Fatalf("expected fill instrument %s, got %s", instB, fill.InstrumentID)
	}
	if fill.Price.Amount.Value != "950" {
		t.Errorf("expected fill price 950 (B bar open), got %s", fill.Price.Amount.Value)
	}
}

// TestEngineEmptyInstrumentIDRejected verifies that an order with empty
// InstrumentID is rejected rather than silently falling back to the current bar's instrument.
func TestEngineEmptyInstrumentIDRejected(t *testing.T) {
	marketKRW := func(v string) market.Price {
		return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}}
	}
	inst := market.InstrumentID("KRX:005930")

	day1 := market.Bar{
		InstrumentID: inst, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("1000"), High: marketKRW("1100"), Low: marketKRW("950"), Close: marketKRW("1050"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "1000"}},
	}
	day2 := market.Bar{
		InstrumentID: inst, Timeframe: market.TimeframeDaily,
		Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
		Open:      marketKRW("1050"), High: marketKRW("1200"), Low: marketKRW("1000"), Close: marketKRW("1150"),
		Volume: market.Quantity{Amount: market.Decimal{Value: "1200"}},
	}
	bars := []market.Bar{day1, day2}

	engine := NewEngine(&testBarSource{bars: bars}, &testStrategyPort{strategy: emptyInstOrderStrategy{id: "test-empty-inst"}})
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID: "paper-empty-inst", FillPolicy: backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}),
		},
		Run: backtest.Run{
			ID: "run-empty-inst",
			Spec: backtest.RunSpec{
				StrategyID: "test-empty-inst", Market: market.MarketKR, Timeframe: market.TimeframeDaily,
				From: day1.Timestamp, To: day2.Timestamp,
			},
		},
	}
	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}

	// Empty InstrumentID → rejected, never retried on day 2
	if len(snap.Rejected) != 1 {
		t.Fatalf("expected 1 rejected order for empty InstrumentID, got %d", len(snap.Rejected))
	}
	if snap.Rejected[0].Reason != "empty order.InstrumentID" {
		t.Errorf("expected reason 'empty order.InstrumentID', got %q", snap.Rejected[0].Reason)
	}
	if len(snap.Fills) != 0 {
		t.Errorf("expected 0 fills for rejected empty-Inst order, got %d", len(snap.Fills))
	}
}

// TestEnginePreservesAccountIdentity verifies that RunRequest.Account.ID is
// preserved in Snapshot.Account.ID regardless of Run.ID.
func TestEnginePreservesAccountIdentity(t *testing.T) {
	bars := makeBars(t)
	const paperID = "my-paper-account-42"
	const runID = "run-unique-run-id"
	engine := NewEngine(&testBarSource{bars: bars}, &testStrategyPort{strategy: simpleBuyStrategy{id: "test-acct"}})
	req := RunRequest{
		Account: backtest.PaperAccount{
			ID: backtest.PaperAccountID(paperID), FillPolicy: backtest.FillPolicyDailyNextBarOHLC,
			RiskSettings: backtest.RiskSettings{AllowShortSelling: false},
			Portfolio:    backtest.NewPortfolioState(market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "10000000"}}),
		},
		Run: backtest.Run{
			ID: backtest.RunID(runID),
			Spec: backtest.RunSpec{
				StrategyID: "test-acct", Market: market.MarketKR, Timeframe: market.TimeframeDaily,
				From: bars[0].Timestamp, To: bars[2].Timestamp,
			},
		},
	}
	snap, err := engine.Run(context.Background(), req)
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}
	if string(snap.Account.ID) != paperID {
		t.Errorf("expected account ID %q, got %q", paperID, snap.Account.ID)
	}
}
