package jobs

import (
	"context"
	"encoding/json"
	"errors"
	"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"
)

func sampleSpec() backtest.RunSpec {
	return backtest.RunSpec{
		StrategyID: "strat-abc",
		Market:     market.MarketKR,
		Timeframe:  market.TimeframeDaily,
		From:       time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
		To:         time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC),
	}
}

func TestBacktestStarterRecordsPendingAndLaunches(t *testing.T) {
	runner := NewRunner()
	store := &stubBacktestRunStore{runs: make(map[backtest.RunID]backtest.Run)}

	var executedPayload json.RawMessage
	runner.Register(KindRunBacktest, func(ctx context.Context, payload json.RawMessage) error {
		executedPayload = payload
		return nil
	})

	fixed := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
	starter := NewBacktestStarter(runner, store,
		WithStarterNow(func() time.Time { return fixed }),
		WithStarterIDGenerator(func() string { return "run-fixed" }),
		WithStarterLaunch(func(fn func()) { fn() }), // run synchronously for determinism
	)

	run, err := starter.StartBacktest(context.Background(), sampleSpec())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if run.ID != "run-fixed" {
		t.Errorf("expected generated id run-fixed, got %q", run.ID)
	}
	if run.Status != backtest.RunStatusPending {
		t.Errorf("expected pending status, got %q", run.Status)
	}
	if !run.CreatedAt.Equal(fixed) || !run.UpdatedAt.Equal(fixed) {
		t.Errorf("expected timestamps %v, got created=%v updated=%v", fixed, run.CreatedAt, run.UpdatedAt)
	}

	// Pending run is persisted before execution so it is immediately listable.
	if len(store.upsertCalls) != 1 || store.upsertCalls[0].Status != backtest.RunStatusPending {
		t.Fatalf("expected one pending upsert, got %+v", store.upsertCalls)
	}

	// The launched job carries a decodable RunBacktestPayload for the same run.
	if executedPayload == nil {
		t.Fatal("expected the runner to be invoked with a payload")
	}
	decoded, err := DecodeRunBacktestPayload(executedPayload)
	if err != nil {
		t.Fatalf("payload should decode: %v", err)
	}
	if decoded.RunID != "run-fixed" {
		t.Errorf("payload run id mismatch: %q", decoded.RunID)
	}
	if decoded.StrategyID != "strat-abc" || decoded.Market != "KR" || decoded.Timeframe != "1d" {
		t.Errorf("payload spec mismatch: %+v", decoded)
	}
}

func TestBacktestStarterPersistErrorStops(t *testing.T) {
	runner := NewRunner()
	launched := false
	runner.Register(KindRunBacktest, func(ctx context.Context, payload json.RawMessage) error {
		launched = true
		return nil
	})

	store := &failingRunStore{err: errors.New("db down")}
	starter := NewBacktestStarter(runner, store,
		WithStarterLaunch(func(fn func()) { fn() }),
	)

	if _, err := starter.StartBacktest(context.Background(), sampleSpec()); err == nil {
		t.Fatal("expected persist error to propagate")
	}
	if launched {
		t.Error("execution must not launch when the pending run cannot be persisted")
	}
}

type failingRunStore struct {
	err error
}

func (f *failingRunStore) UpsertRun(ctx context.Context, run backtest.Run) error { return f.err }
func (f *failingRunStore) GetRun(ctx context.Context, id backtest.RunID) (backtest.Run, error) {
	return backtest.Run{}, storage.ErrRunNotFound
}
