package backtest_test

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

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

// flowStore is an in-memory store that satisfies the instrument, bar, run, and
// result ports the backtest command flow touches. It records every run status
// upsert so the test can assert the pending -> running -> succeeded transition.
type flowStore struct {
	mu           sync.Mutex
	instruments  map[market.InstrumentID]market.Instrument
	bars         map[market.InstrumentID][]market.Bar
	runs         map[domainbacktest.RunID]domainbacktest.Run
	results      map[domainbacktest.RunID]domainbacktest.Result
	runStatusLog []domainbacktest.RunStatus
}

func newFlowStore() *flowStore {
	return &flowStore{
		instruments: make(map[market.InstrumentID]market.Instrument),
		bars:        make(map[market.InstrumentID][]market.Bar),
		runs:        make(map[domainbacktest.RunID]domainbacktest.Run),
		results:     make(map[domainbacktest.RunID]domainbacktest.Result),
	}
}

func (s *flowStore) UpsertInstrument(ctx context.Context, inst market.Instrument) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.instruments[inst.ID] = inst
	return nil
}

func (s *flowStore) GetInstrument(ctx context.Context, id market.InstrumentID) (market.Instrument, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	inst, ok := s.instruments[id]
	if !ok {
		return market.Instrument{}, storage.ErrInstrumentNotFound
	}
	return inst, nil
}

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

func (s *flowStore) UpsertBar(ctx context.Context, bar market.Bar) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.bars[bar.InstrumentID] = append(s.bars[bar.InstrumentID], bar)
	return nil
}

func (s *flowStore) GetBars(ctx context.Context, id market.InstrumentID, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	var out []market.Bar
	for _, bar := range s.bars[id] {
		if bar.Timeframe != timeframe {
			continue
		}
		if bar.Timestamp.Before(from) || bar.Timestamp.After(to) {
			continue
		}
		out = append(out, bar)
	}
	return out, nil
}

func (s *flowStore) UpsertRun(ctx context.Context, run domainbacktest.Run) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.runs[run.ID] = run
	s.runStatusLog = append(s.runStatusLog, run.Status)
	return nil
}

func (s *flowStore) GetRun(ctx context.Context, id domainbacktest.RunID) (domainbacktest.Run, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	run, ok := s.runs[id]
	if !ok {
		return domainbacktest.Run{}, storage.ErrRunNotFound
	}
	return run, nil
}

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

func (s *flowStore) GetResult(ctx context.Context, id domainbacktest.RunID) (domainbacktest.Result, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	result, ok := s.results[id]
	if !ok {
		return domainbacktest.Result{}, storage.ErrResultNotFound
	}
	return result, nil
}

func (s *flowStore) statuses() []domainbacktest.RunStatus {
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make([]domainbacktest.RunStatus, len(s.runStatusLog))
	copy(out, s.runStatusLog)
	return out
}

// TestBacktestCommandFlowFromImportedBars proves the command-first rail end to
// end with storage-backed bars: a started run records a pending run, the runner
// executes it through the bundled strategy resolver against imported daily bars,
// the run reaches terminal succeeded, and the result carries summary fields the
// CLI result step reads. It runs the launch synchronously so no Docker or
// goroutine race is involved.
func TestBacktestCommandFlowFromImportedBars(t *testing.T) {
	ctx := context.Background()
	store := newFlowStore()

	instID := market.InstrumentID("KRX:005930")
	if err := store.UpsertInstrument(ctx, market.Instrument{ID: instID, Market: market.MarketKR}); err != nil {
		t.Fatalf("upsert instrument: %v", err)
	}
	// Imported daily bars: a 3-day window the run spec covers.
	for _, b := range []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"}}},
		{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"}}},
		{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"}}},
	} {
		if err := store.UpsertBar(ctx, b); err != nil {
			t.Fatalf("upsert bar: %v", err)
		}
	}

	engine := workerbacktest.NewEngine(
		workerbacktest.NewStorageBarSource(store, store),
		workerbacktest.NewBuiltInStrategyPort(),
		store,
	)
	runner := jobs.NewRunner()
	jobs.RegisterRunBacktestHandler(runner, store, engine, time.Now)
	starter := jobs.NewBacktestStarter(runner, store,
		jobs.WithStarterIDGenerator(func() string { return "run-flow" }),
		jobs.WithStarterLaunch(func(fn func()) { fn() }), // synchronous for determinism
	)

	spec := domainbacktest.RunSpec{
		StrategyID: workerbacktest.BuiltInStrategyID,
		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),
	}

	run, err := starter.StartBacktest(ctx, spec)
	if err != nil {
		t.Fatalf("start backtest: %v", err)
	}
	if run.ID != "run-flow" {
		t.Fatalf("unexpected run id %q", run.ID)
	}

	// Poll: the run reached terminal succeeded after synchronous execution.
	got, err := store.GetRun(ctx, run.ID)
	if err != nil {
		t.Fatalf("get run: %v", err)
	}
	if got.Status != domainbacktest.RunStatusSucceeded {
		t.Fatalf("expected terminal status succeeded, got %q (statuses=%v)", got.Status, store.statuses())
	}

	// The pending -> running -> succeeded transition is recorded.
	statuses := store.statuses()
	wantTransition := []domainbacktest.RunStatus{
		domainbacktest.RunStatusPending,
		domainbacktest.RunStatusRunning,
		domainbacktest.RunStatusSucceeded,
	}
	for _, want := range wantTransition {
		if !containsStatus(statuses, want) {
			t.Errorf("expected status %q in transition log %v", want, statuses)
		}
	}

	// Result: summary fields the CLI result step reads are present and reflect the
	// buy-and-hold run over the imported bars.
	result, err := store.GetResult(ctx, run.ID)
	if err != nil {
		t.Fatalf("get result: %v", err)
	}
	if result.RunID != run.ID {
		t.Errorf("result run id = %q, want %q", result.RunID, run.ID)
	}
	if result.Summary.StartingCash.Amount.Value != "10000000" {
		t.Errorf("summary starting cash = %q, want 10000000", result.Summary.StartingCash.Amount.Value)
	}
	// Buy 1 unit at 1000 on day 1, hold; ending equity = 9,999,000 cash + 1*1200.
	if result.Summary.EndingEquity.Amount.Value != "10000200" {
		t.Errorf("summary ending equity = %q, want 10000200", result.Summary.EndingEquity.Amount.Value)
	}
	if result.Summary.TradeCount != 1 {
		t.Errorf("summary trade count = %d, want 1", result.Summary.TradeCount)
	}
}

func containsStatus(haystack []domainbacktest.RunStatus, needle domainbacktest.RunStatus) bool {
	for _, s := range haystack {
		if s == needle {
			return true
		}
	}
	return false
}
