package postgres

import (
	"context"
	"errors"
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"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"
)

// newTestStore connects to a local PostgreSQL instance using DATABASE_URL and runs
// the migrations inside a throwaway schema so the test is isolated from any existing
// data or prior migration state in the target database. It skips when no database is
// reachable so unit-only environments stay green; integration coverage runs when the
// local docker-compose Postgres (deployments/local) is up and DATABASE_URL is set.
func newTestStore(t *testing.T) *Store {
	t.Helper()

	dsn := os.Getenv("DATABASE_URL")
	if dsn == "" {
		t.Skip("DATABASE_URL not set; skipping store integration test (start deployments/local Postgres and export DATABASE_URL)")
	}

	ctx := context.Background()
	cfg, err := pgxpool.ParseConfig(dsn)
	if err != nil {
		t.Skipf("DATABASE_URL set but parse failed (%v); skipping store integration test", err)
	}

	// Identifier built only from digits/letters, safe to interpolate.
	schema := fmt.Sprintf("test_analysis_%d", time.Now().UnixNano())
	// Every pooled connection isolates into the throwaway schema.
	cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
		_, err := conn.Exec(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s; SET search_path TO %s", schema, schema))
		return err
	}

	pool, err := pgxpool.NewWithConfig(ctx, cfg)
	if err != nil {
		t.Skipf("DATABASE_URL set but pool create failed (%v); skipping store integration test", err)
	}

	conn, err := pool.Acquire(ctx)
	if err != nil {
		pool.Close()
		t.Skipf("DATABASE_URL set but connection unavailable (%v); skipping store integration test", err)
	}
	if err := RunMigrations(ctx, conn.Conn()); err != nil {
		conn.Release()
		pool.Close()
		t.Fatalf("failed to run migrations: %v", err)
	}
	conn.Release()

	t.Cleanup(func() {
		// Drop the throwaway schema (and its tables) regardless of search_path.
		if c, err := pool.Acquire(ctx); err == nil {
			_, _ = c.Exec(ctx, fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schema))
			c.Release()
		}
		pool.Close()
	})

	return NewStore(pool)
}

func krw(amount string) market.Price {
	return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: amount}}
}

func TestStoreBacktestAnalysisReads(t *testing.T) {
	store := newTestStore(t)
	ctx := context.Background()

	now := time.Now().UTC().Truncate(time.Microsecond)
	// Unique IDs keep the test isolated from any other rows in a shared database.
	suffix := now.Format("20060102T150405.000000000")
	runOlderID := backtest.RunID("test-analysis-older-" + suffix)
	runNewerID := backtest.RunID("test-analysis-newer-" + suffix)
	runNoResultID := backtest.RunID("test-analysis-noresult-" + suffix)

	spec := backtest.RunSpec{
		StrategyID: backtest.StrategyID("strat-analysis"),
		Market:     market.MarketKR,
		Timeframe:  market.TimeframeDaily,
		From:       now.Add(-72 * time.Hour),
		To:         now,
	}

	runOlder := backtest.Run{ID: runOlderID, Spec: spec, Status: backtest.RunStatusSucceeded, CreatedAt: now.Add(-2 * time.Hour), UpdatedAt: now.Add(-2 * time.Hour)}
	runNewer := backtest.Run{ID: runNewerID, Spec: spec, Status: backtest.RunStatusSucceeded, CreatedAt: now.Add(-1 * time.Hour), UpdatedAt: now.Add(-1 * time.Hour)}
	runNoResult := backtest.Run{ID: runNoResultID, Spec: spec, Status: backtest.RunStatusRunning, CreatedAt: now, UpdatedAt: now}

	for _, run := range []backtest.Run{runOlder, runNewer, runNoResult} {
		if err := store.UpsertRun(ctx, run); err != nil {
			t.Fatalf("failed to seed run %s: %v", run.ID, err)
		}
	}

	resultOlder := backtest.Result{
		RunID:        runOlderID,
		StartingCash: krw("10000000"),
		EndingEquity: krw("10000300"),
		Summary:      backtest.SummaryMetrics{StartingCash: krw("10000000"), EndingEquity: krw("10000300"), TotalReturn: market.Decimal{Value: "0.00003"}, TradeCount: 2},
		EquityCurve:  []backtest.EquityPoint{{Timestamp: now.Add(-2 * time.Hour), Equity: krw("10000300")}},
	}
	resultNewer := backtest.Result{
		RunID:        runNewerID,
		StartingCash: krw("10000000"),
		EndingEquity: krw("9999000"),
		Summary:      backtest.SummaryMetrics{StartingCash: krw("10000000"), EndingEquity: krw("9999000"), TotalReturn: market.Decimal{Value: "-0.0001"}, TradeCount: 1},
		EquityCurve:  []backtest.EquityPoint{{Timestamp: now.Add(-1 * time.Hour), Equity: krw("9999000")}},
	}
	for _, res := range []backtest.Result{resultOlder, resultNewer} {
		if err := store.UpsertResult(ctx, res); err != nil {
			t.Fatalf("failed to seed result %s: %v", res.RunID, err)
		}
	}

	// ListRuns returns newest CreatedAt first: runNewer must appear before runOlder.
	runs, err := store.ListRuns(ctx, "")
	if err != nil {
		t.Fatalf("ListRuns returned error: %v", err)
	}
	idxNewer, idxOlder := indexOfRun(runs, runNewerID), indexOfRun(runs, runOlderID)
	if idxNewer < 0 || idxOlder < 0 {
		t.Fatalf("ListRuns missing seeded runs: newer=%d older=%d", idxNewer, idxOlder)
	}
	if idxNewer > idxOlder {
		t.Errorf("ListRuns ordering: newer run at %d should precede older run at %d", idxNewer, idxOlder)
	}

	// ListRuns filters by status: the running run appears, succeeded runs do not.
	running, err := store.ListRuns(ctx, backtest.RunStatusRunning)
	if err != nil {
		t.Fatalf("ListRuns(running) returned error: %v", err)
	}
	if indexOfRun(running, runNoResultID) < 0 {
		t.Errorf("ListRuns(running) should include the running run")
	}
	if indexOfRun(running, runNewerID) >= 0 {
		t.Errorf("ListRuns(running) should not include a succeeded run")
	}

	// GetRunDetail for a run without a result reports HasResult=false and no error.
	detail, err := store.GetRunDetail(ctx, runNoResultID)
	if err != nil {
		t.Fatalf("GetRunDetail(no-result) returned error: %v", err)
	}
	if detail.HasResult {
		t.Errorf("GetRunDetail(no-result) HasResult should be false")
	}
	if detail.Run.ID != runNoResultID {
		t.Errorf("GetRunDetail(no-result) run id mismatch: got %s", detail.Run.ID)
	}

	// GetRunDetail for a run with a result returns it.
	detailWith, err := store.GetRunDetail(ctx, runNewerID)
	if err != nil {
		t.Fatalf("GetRunDetail(with-result) returned error: %v", err)
	}
	if !detailWith.HasResult || detailWith.Result.Summary.TradeCount != 1 {
		t.Errorf("GetRunDetail(with-result) unexpected: %+v", detailWith)
	}

	// CompareResults preserves the caller's input order.
	compared, err := store.CompareResults(ctx, []backtest.RunID{runNewerID, runOlderID})
	if err != nil {
		t.Fatalf("CompareResults returned error: %v", err)
	}
	if len(compared) != 2 {
		t.Fatalf("CompareResults length: got %d, want 2", len(compared))
	}
	if compared[0].RunID != runNewerID || compared[1].RunID != runOlderID {
		t.Errorf("CompareResults order not preserved: got %s, %s", compared[0].RunID, compared[1].RunID)
	}

	// CompareResults propagates a missing result as ErrResultNotFound.
	_, err = store.CompareResults(ctx, []backtest.RunID{runNewerID, runNoResultID})
	if !errors.Is(err, storage.ErrResultNotFound) {
		t.Errorf("CompareResults missing result: expected ErrResultNotFound, got %v", err)
	}
}

func indexOfRun(runs []backtest.Run, id backtest.RunID) int {
	for i, run := range runs {
		if run.ID == id {
			return i
		}
	}
	return -1
}
