package postgres

import (
	"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/postgres/sqlc"
)

func TestBacktestRunMappingRoundTrip(t *testing.T) {
	now := time.Now().UTC().Truncate(time.Microsecond) // Truncate because postgres timestamptz resolves to microsecond precision
	run := backtest.Run{
		ID: backtest.RunID("run-123"),
		Spec: backtest.RunSpec{
			StrategyID: backtest.StrategyID("strat-abc"),
			Market:     market.MarketKR,
			Timeframe:  market.TimeframeDaily,
			From:       now.Add(-24 * time.Hour),
			To:         now,
		},
		Status:    backtest.RunStatusSucceeded,
		CreatedAt: now.Add(-25 * time.Hour),
		UpdatedAt: now,
	}

	params, err := mapRunToParams(run)
	if err != nil {
		t.Fatalf("failed to map run to params: %v", err)
	}

	row := sqlc.BacktestRun{
		ID:         params.ID,
		StrategyID: params.StrategyID,
		Market:     params.Market,
		Timeframe:  params.Timeframe,
		FromTime:   params.FromTime,
		ToTime:     params.ToTime,
		Status:     params.Status,
		CreatedAt:  params.CreatedAt,
		UpdatedAt:  params.UpdatedAt,
	}

	mappedBack, err := mapRowToRun(row)
	if err != nil {
		t.Fatalf("failed to map row back to run: %v", err)
	}

	if mappedBack.ID != run.ID {
		t.Errorf("ID mismatch: got %v, want %v", mappedBack.ID, run.ID)
	}
	if mappedBack.Spec.StrategyID != run.Spec.StrategyID {
		t.Errorf("StrategyID mismatch: got %v, want %v", mappedBack.Spec.StrategyID, run.Spec.StrategyID)
	}
	if mappedBack.Spec.Market != run.Spec.Market {
		t.Errorf("Market mismatch: got %v, want %v", mappedBack.Spec.Market, run.Spec.Market)
	}
	if mappedBack.Spec.Timeframe != run.Spec.Timeframe {
		t.Errorf("Timeframe mismatch: got %v, want %v", mappedBack.Spec.Timeframe, run.Spec.Timeframe)
	}
	if !mappedBack.Spec.From.Equal(run.Spec.From) {
		t.Errorf("From mismatch: got %v, want %v", mappedBack.Spec.From, run.Spec.From)
	}
	if !mappedBack.Spec.To.Equal(run.Spec.To) {
		t.Errorf("To mismatch: got %v, want %v", mappedBack.Spec.To, run.Spec.To)
	}
	if mappedBack.Status != run.Status {
		t.Errorf("Status mismatch: got %v, want %v", mappedBack.Status, run.Status)
	}
	if !mappedBack.CreatedAt.Equal(run.CreatedAt) {
		t.Errorf("CreatedAt mismatch: got %v, want %v", mappedBack.CreatedAt, run.CreatedAt)
	}
	if !mappedBack.UpdatedAt.Equal(run.UpdatedAt) {
		t.Errorf("UpdatedAt mismatch: got %v, want %v", mappedBack.UpdatedAt, run.UpdatedAt)
	}
}

func TestMarketBarMappingRejectsInvalidDecimal(t *testing.T) {
	now := time.Now().UTC()
	bar := market.Bar{
		InstrumentID: market.InstrumentID("KRX:005930"),
		Timeframe:    market.TimeframeDaily,
		Timestamp:    now,
		Open: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "invalid_decimal"},
		},
		High: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "50000"},
		},
		Low: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "49000"},
		},
		Close: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "49500"},
		},
		Volume: market.Quantity{
			Amount: market.Decimal{Value: "1000000"},
		},
	}

	_, err := mapBarToParams(bar)
	if err == nil {
		t.Fatal("expected error when mapping bar with invalid open decimal, got nil")
	}

	// Make Open valid but Volume invalid
	bar.Open.Amount.Value = "50000"
	bar.Volume.Amount.Value = "abc"
	_, err = mapBarToParams(bar)
	if err == nil {
		t.Fatal("expected error when mapping bar with invalid volume decimal, got nil")
	}
}

func TestBacktestResultMappingRoundTrip(t *testing.T) {
	now := time.Now().UTC().Truncate(time.Second)
	res := backtest.Result{
		RunID: backtest.RunID("run-123"),
		StartingCash: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "100000000"},
		},
		EndingEquity: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "123456789.12"},
		},
		Trades: []backtest.TradeSummary{
			{
				InstrumentID: market.InstrumentID("KRX:005930"),
				Side:         backtest.OrderSideBuy,
				Quantity:     market.Quantity{Amount: market.Decimal{Value: "10"}},
				Price:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "50000"}},
				Timestamp:    now.Add(-1 * time.Hour),
			},
		},
		Positions: []backtest.PositionSummary{
			{
				InstrumentID: market.InstrumentID("KRX:005930"),
				Quantity:     market.Quantity{Amount: market.Decimal{Value: "10"}},
				LastPrice:    market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "52000"}},
			},
		},
		Summary: backtest.SummaryMetrics{
			StartingCash: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100000000"}},
			EndingEquity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "123456789.12"}},
			TotalReturn:  market.Decimal{Value: "0.2345678912"},
			TradeCount:   1,
		},
		EquityCurve: []backtest.EquityPoint{
			{Timestamp: now.Add(-2 * time.Hour), Equity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100000000"}}},
			{Timestamp: now.Add(-1 * time.Hour), Equity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "123456789.12"}}},
		},
	}

	params, err := mapResultToParams(res)
	if err != nil {
		t.Fatalf("failed to map result to params: %v", err)
	}

	row := sqlc.BacktestResult{
		RunID:                params.RunID,
		StartingCashCurrency: params.StartingCashCurrency,
		StartingCashAmount:   params.StartingCashAmount,
		EndingEquityCurrency: params.EndingEquityCurrency,
		EndingEquityAmount:   params.EndingEquityAmount,
		TotalReturn:          params.TotalReturn,
		TradeCount:           params.TradeCount,
		Trades:               params.Trades,
		Positions:            params.Positions,
		EquityCurve:          params.EquityCurve,
	}

	mappedBack, err := mapRowToResult(row)
	if err != nil {
		t.Fatalf("failed to map row back to result: %v", err)
	}

	if mappedBack.RunID != res.RunID {
		t.Errorf("RunID mismatch: got %v, want %v", mappedBack.RunID, res.RunID)
	}
	if mappedBack.StartingCash.Currency != res.StartingCash.Currency || mappedBack.StartingCash.Amount.Value != res.StartingCash.Amount.Value {
		t.Errorf("StartingCash mismatch: got %v, want %v", mappedBack.StartingCash, res.StartingCash)
	}
	if mappedBack.EndingEquity.Currency != res.EndingEquity.Currency || mappedBack.EndingEquity.Amount.Value != res.EndingEquity.Amount.Value {
		t.Errorf("EndingEquity mismatch: got %v, want %v", mappedBack.EndingEquity, res.EndingEquity)
	}
	if len(mappedBack.Trades) != len(res.Trades) {
		t.Fatalf("Trades length mismatch: got %d, want %d", len(mappedBack.Trades), len(res.Trades))
	}
	if mappedBack.Trades[0].InstrumentID != res.Trades[0].InstrumentID || mappedBack.Trades[0].Side != res.Trades[0].Side || mappedBack.Trades[0].Quantity.Amount.Value != res.Trades[0].Quantity.Amount.Value {
		t.Errorf("Trade mismatch: got %v, want %v", mappedBack.Trades[0], res.Trades[0])
	}
	if !mappedBack.Trades[0].Timestamp.Equal(res.Trades[0].Timestamp) {
		t.Errorf("Trade timestamp mismatch: got %v, want %v", mappedBack.Trades[0].Timestamp, res.Trades[0].Timestamp)
	}

	if len(mappedBack.Positions) != len(res.Positions) {
		t.Fatalf("Positions length mismatch: got %d, want %d", len(mappedBack.Positions), len(res.Positions))
	}
	if mappedBack.Positions[0].InstrumentID != res.Positions[0].InstrumentID || mappedBack.Positions[0].Quantity.Amount.Value != res.Positions[0].Quantity.Amount.Value {
		t.Errorf("Position mismatch: got %v, want %v", mappedBack.Positions[0], res.Positions[0])
	}

	// Summary round-trips, reconstructed from persisted columns.
	if mappedBack.Summary.TotalReturn.Value != res.Summary.TotalReturn.Value {
		t.Errorf("Summary TotalReturn mismatch: got %q, want %q", mappedBack.Summary.TotalReturn.Value, res.Summary.TotalReturn.Value)
	}
	if mappedBack.Summary.TradeCount != res.Summary.TradeCount {
		t.Errorf("Summary TradeCount mismatch: got %d, want %d", mappedBack.Summary.TradeCount, res.Summary.TradeCount)
	}
	if mappedBack.Summary.StartingCash.Amount.Value != res.Summary.StartingCash.Amount.Value || mappedBack.Summary.EndingEquity.Amount.Value != res.Summary.EndingEquity.Amount.Value {
		t.Errorf("Summary cash/equity mismatch: got %+v, want %+v", mappedBack.Summary, res.Summary)
	}

	// Equity curve round-trips with order and timestamps preserved.
	if len(mappedBack.EquityCurve) != len(res.EquityCurve) {
		t.Fatalf("EquityCurve length mismatch: got %d, want %d", len(mappedBack.EquityCurve), len(res.EquityCurve))
	}
	for i := range res.EquityCurve {
		if !mappedBack.EquityCurve[i].Timestamp.Equal(res.EquityCurve[i].Timestamp) {
			t.Errorf("EquityCurve[%d] timestamp mismatch: got %v, want %v", i, mappedBack.EquityCurve[i].Timestamp, res.EquityCurve[i].Timestamp)
		}
		if mappedBack.EquityCurve[i].Equity.Amount.Value != res.EquityCurve[i].Equity.Amount.Value {
			t.Errorf("EquityCurve[%d] equity mismatch: got %q, want %q", i, mappedBack.EquityCurve[i].Equity.Amount.Value, res.EquityCurve[i].Equity.Amount.Value)
		}
	}
}

func TestBacktestResultMappingDefaultsEmptySummary(t *testing.T) {
	// A result persisted without a computed summary or equity curve must still map
	// to valid NOT NULL columns ("0" total return, empty JSON arrays).
	res := backtest.Result{
		RunID:        backtest.RunID("run-empty"),
		StartingCash: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100"}},
		EndingEquity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100"}},
	}

	params, err := mapResultToParams(res)
	if err != nil {
		t.Fatalf("failed to map result to params: %v", err)
	}
	if string(params.EquityCurve) != "[]" {
		t.Errorf("expected empty equity curve JSON array, got %q", string(params.EquityCurve))
	}

	mappedBack, err := mapRowToResult(sqlc.BacktestResult{
		RunID:                params.RunID,
		StartingCashCurrency: params.StartingCashCurrency,
		StartingCashAmount:   params.StartingCashAmount,
		EndingEquityCurrency: params.EndingEquityCurrency,
		EndingEquityAmount:   params.EndingEquityAmount,
		TotalReturn:          params.TotalReturn,
		TradeCount:           params.TradeCount,
		Trades:               params.Trades,
		Positions:            params.Positions,
		EquityCurve:          params.EquityCurve,
	})
	if err != nil {
		t.Fatalf("failed to map row back to result: %v", err)
	}
	if mappedBack.Summary.TotalReturn.Value != "0" {
		t.Errorf("expected default total return 0, got %q", mappedBack.Summary.TotalReturn.Value)
	}
	if len(mappedBack.EquityCurve) != 0 {
		t.Errorf("expected empty equity curve, got %d points", len(mappedBack.EquityCurve))
	}
}

func TestBacktestRunListRowMapping(t *testing.T) {
	// ListRuns returns sqlc.BacktestRun rows; verify they map to domain runs the same
	// way single-run detail rows do, so list and detail surfaces stay consistent.
	now := time.Now().UTC().Truncate(time.Microsecond)
	run := backtest.Run{
		ID: backtest.RunID("run-list"),
		Spec: backtest.RunSpec{
			StrategyID: backtest.StrategyID("strat-list"),
			Market:     market.MarketUS,
			Timeframe:  market.TimeframeDaily,
			From:       now.Add(-48 * time.Hour),
			To:         now,
		},
		Status:    backtest.RunStatusRunning,
		CreatedAt: now.Add(-49 * time.Hour),
		UpdatedAt: now,
	}

	params, err := mapRunToParams(run)
	if err != nil {
		t.Fatalf("failed to map run to params: %v", err)
	}

	mappedBack, err := mapRowToRun(sqlc.BacktestRun{
		ID:         params.ID,
		StrategyID: params.StrategyID,
		Market:     params.Market,
		Timeframe:  params.Timeframe,
		FromTime:   params.FromTime,
		ToTime:     params.ToTime,
		Status:     params.Status,
		CreatedAt:  params.CreatedAt,
		UpdatedAt:  params.UpdatedAt,
	})
	if err != nil {
		t.Fatalf("failed to map list row to run: %v", err)
	}
	if mappedBack.ID != run.ID || mappedBack.Status != run.Status || mappedBack.Spec.Market != run.Spec.Market {
		t.Errorf("list run mapping mismatch: got %+v, want %+v", mappedBack, run)
	}
}

func TestBacktestResultMappingRejectsInvalidDecimal(t *testing.T) {
	res := backtest.Result{
		RunID: backtest.RunID("run-123"),
		StartingCash: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "invalid"},
		},
		EndingEquity: market.Price{
			Currency: market.CurrencyKRW,
			Amount:   market.Decimal{Value: "12345"},
		},
	}

	_, err := mapResultToParams(res)
	if err == nil {
		t.Fatal("expected error when mapping result with invalid starting cash decimal, got nil")
	}

	res.StartingCash.Amount.Value = "100"
	res.EndingEquity.Amount.Value = "invalid"
	_, err = mapResultToParams(res)
	if err == nil {
		t.Fatal("expected error when mapping result with invalid ending equity decimal, got nil")
	}
}

func TestInstrumentMappingRoundTrip(t *testing.T) {
	inst := market.Instrument{
		ID:        "NASDAQ:AAPL",
		Market:    market.MarketUS,
		Venue:     market.VenueNASDAQ,
		Symbol:    "AAPL",
		Name:      "Apple Inc.",
		Currency:  market.CurrencyUSD,
		AssetType: market.AssetTypeEquity,
		ProviderSymbols: map[string]string{
			"kis": "AAPL",
		},
	}

	params, err := mapInstrumentToParams(inst)
	if err != nil {
		t.Fatalf("failed to map instrument to params: %v", err)
	}

	row := sqlc.Instrument{
		ID:              params.ID,
		Market:          params.Market,
		Venue:           params.Venue,
		Symbol:          params.Symbol,
		Name:            params.Name,
		Currency:        params.Currency,
		ProviderSymbols: params.ProviderSymbols,
		AssetType:       params.AssetType,
	}

	mappedBack, err := mapRowToInstrument(row)
	if err != nil {
		t.Fatalf("failed to map row back to instrument: %v", err)
	}

	if mappedBack.ID != inst.ID {
		t.Errorf("ID mismatch: got %v, want %v", mappedBack.ID, inst.ID)
	}
	if mappedBack.Market != inst.Market {
		t.Errorf("Market mismatch: got %v, want %v", mappedBack.Market, inst.Market)
	}
	if mappedBack.Venue != inst.Venue {
		t.Errorf("Venue mismatch: got %v, want %v", mappedBack.Venue, inst.Venue)
	}
	if mappedBack.Symbol != inst.Symbol {
		t.Errorf("Symbol mismatch: got %v, want %v", mappedBack.Symbol, inst.Symbol)
	}
	if mappedBack.Name != inst.Name {
		t.Errorf("Name mismatch: got %v, want %v", mappedBack.Name, inst.Name)
	}
	if mappedBack.Currency != inst.Currency {
		t.Errorf("Currency mismatch: got %v, want %v", mappedBack.Currency, inst.Currency)
	}
	if mappedBack.AssetType != inst.AssetType {
		t.Errorf("AssetType mismatch: got %v, want %v", mappedBack.AssetType, inst.AssetType)
	}
	if mappedBack.ProviderSymbols["kis"] != inst.ProviderSymbols["kis"] {
		t.Errorf("ProviderSymbols mismatch: got %+v, want %+v", mappedBack.ProviderSymbols, inst.ProviderSymbols)
	}
}
