package jobs

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

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

type stubBacktestRunStore struct {
	runs        map[backtest.RunID]backtest.Run
	upsertCalls []backtest.Run
	getError    error
}

func (s *stubBacktestRunStore) UpsertRun(ctx context.Context, run backtest.Run) error {
	s.runs[run.ID] = run
	s.upsertCalls = append(s.upsertCalls, run)
	return nil
}

func (s *stubBacktestRunStore) GetRun(ctx context.Context, id backtest.RunID) (backtest.Run, error) {
	if s.getError != nil {
		return backtest.Run{}, s.getError
	}
	run, ok := s.runs[id]
	if !ok {
		return backtest.Run{}, storage.ErrRunNotFound
	}
	return run, nil
}

type stubBacktestExecutor struct {
	called bool
	run    backtest.Run
	err    error
}

func (s *stubBacktestExecutor) Execute(ctx context.Context, run backtest.Run) error {
	s.called = true
	s.run = run
	return s.err
}

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

	fixedTime := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	nowFunc := func() time.Time { return fixedTime }

	RegisterRunBacktestHandler(runner, store, executor, nowFunc)

	payload := json.RawMessage(`{
		"run_id": "run-123",
		"strategy_id": "strat-abc",
		"market": "KR",
		"timeframe": "1d",
		"from": "2026-05-01T00:00:00Z",
		"to": "2026-05-15T00:00:00Z"
	}`)

	job := Job{
		ID:      "job-1",
		Kind:    KindRunBacktest,
		Payload: payload,
	}

	err := runner.Execute(context.Background(), job)
	if err != nil {
		t.Fatalf("unexpected error executing job: %v", err)
	}

	if !executor.called {
		t.Fatal("expected executor to be called, but it was not")
	}

	// Verify transitions (new run: pending -> running -> succeeded)
	if len(store.upsertCalls) != 3 {
		t.Fatalf("expected 3 UpsertRun calls, got %d", len(store.upsertCalls))
	}

	if store.upsertCalls[0].Status != backtest.RunStatusPending {
		t.Errorf("expected first transition to be pending, got %s", store.upsertCalls[0].Status)
	}

	if store.upsertCalls[1].Status != backtest.RunStatusRunning {
		t.Errorf("expected second transition to be running, got %s", store.upsertCalls[1].Status)
	}

	if store.upsertCalls[2].Status != backtest.RunStatusSucceeded {
		t.Errorf("expected third transition to be succeeded, got %s", store.upsertCalls[2].Status)
	}
}

func TestRegisterRunBacktestHandlerTransitionsFailed(t *testing.T) {
	runner := NewRunner()
	store := &stubBacktestRunStore{runs: make(map[backtest.RunID]backtest.Run)}
	executor := &stubBacktestExecutor{err: errors.New("execution failed")}

	fixedTime := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	nowFunc := func() time.Time { return fixedTime }

	RegisterRunBacktestHandler(runner, store, executor, nowFunc)

	payload := json.RawMessage(`{
		"run_id": "run-123",
		"strategy_id": "strat-abc",
		"market": "KR",
		"timeframe": "1d",
		"from": "2026-05-01T00:00:00Z",
		"to": "2026-05-15T00:00:00Z"
	}`)

	job := Job{
		ID:      "job-1",
		Kind:    KindRunBacktest,
		Payload: payload,
	}

	err := runner.Execute(context.Background(), job)
	if err == nil {
		t.Fatal("expected executor error, got nil")
	}

	if !executor.called {
		t.Fatal("expected executor to be called, but it was not")
	}

	// Verify transitions (new run: pending -> running -> failed)
	if len(store.upsertCalls) != 3 {
		t.Fatalf("expected 3 UpsertRun calls, got %d", len(store.upsertCalls))
	}

	if store.upsertCalls[0].Status != backtest.RunStatusPending {
		t.Errorf("expected first transition to be pending, got %s", store.upsertCalls[0].Status)
	}

	if store.upsertCalls[1].Status != backtest.RunStatusRunning {
		t.Errorf("expected second transition to be running, got %s", store.upsertCalls[1].Status)
	}

	if store.upsertCalls[2].Status != backtest.RunStatusFailed {
		t.Errorf("expected third transition to be failed, got %s", store.upsertCalls[2].Status)
	}
}

func TestRegisterRunBacktestHandlerTransitionsCanceled(t *testing.T) {
	runner := NewRunner()
	store := &stubBacktestRunStore{runs: make(map[backtest.RunID]backtest.Run)}
	executor := &stubBacktestExecutor{err: context.Canceled}

	fixedTime := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	nowFunc := func() time.Time { return fixedTime }

	RegisterRunBacktestHandler(runner, store, executor, nowFunc)

	payload := json.RawMessage(`{
		"run_id": "run-123",
		"strategy_id": "strat-abc",
		"market": "KR",
		"timeframe": "1d",
		"from": "2026-05-01T00:00:00Z",
		"to": "2026-05-15T00:00:00Z"
	}`)

	job := Job{
		ID:      "job-1",
		Kind:    KindRunBacktest,
		Payload: payload,
	}

	err := runner.Execute(context.Background(), job)
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("expected context.Canceled error, got %v", err)
	}

	if !executor.called {
		t.Fatal("expected executor to be called, but it was not")
	}

	if len(store.upsertCalls) != 3 {
		t.Fatalf("expected 3 UpsertRun calls, got %d", len(store.upsertCalls))
	}

	if store.upsertCalls[0].Status != backtest.RunStatusPending {
		t.Errorf("expected first transition to be pending, got %s", store.upsertCalls[0].Status)
	}

	if store.upsertCalls[1].Status != backtest.RunStatusRunning {
		t.Errorf("expected second transition to be running, got %s", store.upsertCalls[1].Status)
	}

	if store.upsertCalls[2].Status != backtest.RunStatusCanceled {
		t.Errorf("expected third transition to be canceled, got %s", store.upsertCalls[2].Status)
	}
}

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

	fixedTime := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	nowFunc := func() time.Time { return fixedTime }

	// Seed the run already in the store
	runID := backtest.RunID("run-123")
	store.runs[runID] = backtest.Run{
		ID:        runID,
		Status:    backtest.RunStatusPending,
		CreatedAt: fixedTime.Add(-time.Hour),
		UpdatedAt: fixedTime.Add(-time.Hour),
	}

	RegisterRunBacktestHandler(runner, store, executor, nowFunc)

	payload := json.RawMessage(`{
		"run_id": "run-123",
		"strategy_id": "strat-abc",
		"market": "KR",
		"timeframe": "1d",
		"from": "2026-05-01T00:00:00Z",
		"to": "2026-05-15T00:00:00Z"
	}`)

	job := Job{
		ID:      "job-1",
		Kind:    KindRunBacktest,
		Payload: payload,
	}

	err := runner.Execute(context.Background(), job)
	if err != nil {
		t.Fatalf("unexpected error executing job: %v", err)
	}

	if !executor.called {
		t.Fatal("expected executor to be called, but it was not")
	}

	// Verify transitions (existing run: running -> succeeded)
	if len(store.upsertCalls) != 2 {
		t.Fatalf("expected 2 UpsertRun calls, got %d", len(store.upsertCalls))
	}

	if store.upsertCalls[0].Status != backtest.RunStatusRunning {
		t.Errorf("expected first transition to be running, got %s", store.upsertCalls[0].Status)
	}

	if store.upsertCalls[1].Status != backtest.RunStatusSucceeded {
		t.Errorf("expected second transition to be succeeded, got %s", store.upsertCalls[1].Status)
	}

	// Check CreatedAt was preserved
	if !store.runs[runID].CreatedAt.Equal(fixedTime.Add(-time.Hour)) {
		t.Errorf("expected CreatedAt to be preserved as %v, got %v", fixedTime.Add(-time.Hour), store.runs[runID].CreatedAt)
	}
}

func TestRegisterRunBacktestHandlerStoreError(t *testing.T) {
	runner := NewRunner()
	store := &stubBacktestRunStore{
		runs:     make(map[backtest.RunID]backtest.Run),
		getError: errors.New("hard db error"),
	}
	executor := &stubBacktestExecutor{}

	fixedTime := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	nowFunc := func() time.Time { return fixedTime }

	RegisterRunBacktestHandler(runner, store, executor, nowFunc)

	payload := json.RawMessage(`{
		"run_id": "run-123",
		"strategy_id": "strat-abc",
		"market": "KR",
		"timeframe": "1d",
		"from": "2026-05-01T00:00:00Z",
		"to": "2026-05-15T00:00:00Z"
	}`)

	job := Job{
		ID:      "job-1",
		Kind:    KindRunBacktest,
		Payload: payload,
	}

	err := runner.Execute(context.Background(), job)
	if err == nil {
		t.Fatal("expected error from hard DB error, got nil")
	}

	if executor.called {
		t.Fatal("expected executor not to be called on store error")
	}

	if len(store.upsertCalls) != 0 {
		t.Fatalf("expected 0 UpsertRun calls on store error, got %d", len(store.upsertCalls))
	}
}

func TestRegisterRunBacktestHandlerOverridesBuiltinPlaceholder(t *testing.T) {
	runner := NewRunner()
	RegisterBuiltins(runner)

	store := &stubBacktestRunStore{runs: make(map[backtest.RunID]backtest.Run)}
	executor := &stubBacktestExecutor{}

	fixedTime := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	nowFunc := func() time.Time { return fixedTime }

	RegisterRunBacktestHandler(runner, store, executor, nowFunc)

	// Ensure length remains 3 (overrode KindRunBacktest rather than adding a new kind)
	if runner.Len() != 3 {
		t.Errorf("expected runner length to remain 3, got %d", runner.Len())
	}

	payload := json.RawMessage(`{
		"run_id": "run-123",
		"strategy_id": "strat-abc",
		"market": "KR",
		"timeframe": "1d",
		"from": "2026-05-01T00:00:00Z",
		"to": "2026-05-15T00:00:00Z"
	}`)

	job := Job{
		ID:      "job-1",
		Kind:    KindRunBacktest,
		Payload: payload,
	}

	err := runner.Execute(context.Background(), job)
	if err != nil {
		t.Fatalf("unexpected error executing job: %v", err)
	}

	if !executor.called {
		t.Fatal("expected live executor to be called (placeholder overrode), but it was not")
	}
}

func TestDecodeRunBacktestPayloadRejectsMissingFields(t *testing.T) {
	tests := []struct {
		name    string
		payload string
	}{
		{"missing run_id", `{"strategy_id":"strat-abc","market":"KR","timeframe":"1d","from":"20260501","to":"20260515"}`},
		{"missing strategy_id", `{"run_id":"run-123","market":"KR","timeframe":"1d","from":"20260501","to":"20260515"}`},
		{"missing market", `{"run_id":"run-123","strategy_id":"strat-abc","timeframe":"1d","from":"20260501","to":"20260515"}`},
		{"missing timeframe", `{"run_id":"run-123","strategy_id":"strat-abc","market":"KR","from":"20260501","to":"20260515"}`},
		{"missing from", `{"run_id":"run-123","strategy_id":"strat-abc","market":"KR","timeframe":"1d","to":"20260515"}`},
		{"missing to", `{"run_id":"run-123","strategy_id":"strat-abc","market":"KR","timeframe":"1d","from":"20260501"}`},
		{"inverted dates", `{"run_id":"run-123","strategy_id":"strat-abc","market":"KR","timeframe":"1d","from":"20260515","to":"20260501"}`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := DecodeRunBacktestPayload(json.RawMessage(tt.payload))
			if err == nil {
				t.Error("expected decoder error, got nil")
			}
		})
	}
}
