package socket

import (
	"testing"
	"time"

	altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
	"git.toki-labs.com/toki/alt/packages/domain/backtest"
	"git.toki-labs.com/toki/alt/packages/domain/market"
)

func TestRunSpecFromProtoValid(t *testing.T) {
	from := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
	to := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC)

	spec, err := runSpecFromProto(&altv1.BacktestRunSpec{
		StrategyId: "strat-abc",
		Market:     altv1.Market_MARKET_KR,
		Timeframe:  altv1.Timeframe_TIMEFRAME_DAILY,
		FromUnixMs: from.UnixMilli(),
		ToUnixMs:   to.UnixMilli(),
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if spec.StrategyID != "strat-abc" {
		t.Errorf("strategy id mismatch: got %q", spec.StrategyID)
	}
	if spec.Market != market.MarketKR {
		t.Errorf("market mismatch: got %q", spec.Market)
	}
	if spec.Timeframe != market.TimeframeDaily {
		t.Errorf("timeframe mismatch: got %q", spec.Timeframe)
	}
	if !spec.From.Equal(from) {
		t.Errorf("from mismatch: got %v want %v", spec.From, from)
	}
	if !spec.To.Equal(to) {
		t.Errorf("to mismatch: got %v want %v", spec.To, to)
	}
}

func TestRunSpecFromProtoRejectsInvalid(t *testing.T) {
	validFrom := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC).UnixMilli()
	validTo := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC).UnixMilli()

	tests := []struct {
		name string
		spec *altv1.BacktestRunSpec
	}{
		{"nil spec", nil},
		{"missing strategy", &altv1.BacktestRunSpec{Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validFrom, ToUnixMs: validTo}},
		{"unspecified market", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_UNSPECIFIED, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validFrom, ToUnixMs: validTo}},
		{"unspecified timeframe", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_UNSPECIFIED, FromUnixMs: validFrom, ToUnixMs: validTo}},
		{"missing from", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, ToUnixMs: validTo}},
		{"missing to", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validFrom}},
		{"inverted range", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validTo, ToUnixMs: validFrom}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if _, err := runSpecFromProto(tt.spec); err == nil {
				t.Error("expected error, got nil")
			}
		})
	}
}

func TestMarketEnumRoundTrip(t *testing.T) {
	for _, m := range []market.Market{market.MarketKR, market.MarketUS} {
		got, err := marketFromProto(marketToProto(m))
		if err != nil {
			t.Fatalf("unexpected error for %q: %v", m, err)
		}
		if got != m {
			t.Errorf("market round-trip mismatch: got %q want %q", got, m)
		}
	}
}

func TestTimeframeEnumRoundTrip(t *testing.T) {
	for _, tf := range []market.Timeframe{market.TimeframeDaily, market.TimeframeMin1, market.TimeframeMin5} {
		got, err := timeframeFromProto(timeframeToProto(tf))
		if err != nil {
			t.Fatalf("unexpected error for %q: %v", tf, err)
		}
		if got != tf {
			t.Errorf("timeframe round-trip mismatch: got %q want %q", got, tf)
		}
	}
}

func TestRunStatusEnumMapping(t *testing.T) {
	statuses := []backtest.RunStatus{
		backtest.RunStatusPending,
		backtest.RunStatusRunning,
		backtest.RunStatusSucceeded,
		backtest.RunStatusFailed,
		backtest.RunStatusCanceled,
	}
	for _, s := range statuses {
		if got := runStatusFromProto(runStatusToProto(s)); got != s {
			t.Errorf("status round-trip mismatch: got %q want %q", got, s)
		}
	}

	// Unspecified maps to the empty status so the store lists all runs.
	if got := runStatusFromProto(altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_UNSPECIFIED); got != "" {
		t.Errorf("unspecified status should map to empty, got %q", got)
	}
}

func TestRunToProto(t *testing.T) {
	created := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC)
	updated := time.Date(2026, 5, 30, 11, 0, 0, 0, time.UTC)
	run := backtest.Run{
		ID: "run-1",
		Spec: backtest.RunSpec{
			StrategyID: "strat-abc",
			Market:     market.MarketUS,
			Timeframe:  market.TimeframeMin5,
			From:       time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
			To:         time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC),
		},
		Status:    backtest.RunStatusRunning,
		CreatedAt: created,
		UpdatedAt: updated,
	}

	got := runToProto(run)
	if got.GetId() != "run-1" {
		t.Errorf("id mismatch: %q", got.GetId())
	}
	if got.GetStatus() != altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_RUNNING {
		t.Errorf("status mismatch: %v", got.GetStatus())
	}
	if got.GetSpec().GetMarket() != altv1.Market_MARKET_US {
		t.Errorf("market mismatch: %v", got.GetSpec().GetMarket())
	}
	if got.GetCreatedAtUnixMs() != created.UnixMilli() {
		t.Errorf("created mismatch: %d", got.GetCreatedAtUnixMs())
	}
	if got.GetUpdatedAtUnixMs() != updated.UnixMilli() {
		t.Errorf("updated mismatch: %d", got.GetUpdatedAtUnixMs())
	}
}

func TestRunToProtoZeroTimeMapsToZero(t *testing.T) {
	got := runToProto(backtest.Run{ID: "run-1", Status: backtest.RunStatusPending})
	if got.GetCreatedAtUnixMs() != 0 {
		t.Errorf("zero created time should map to 0, got %d", got.GetCreatedAtUnixMs())
	}
	if got.GetUpdatedAtUnixMs() != 0 {
		t.Errorf("zero updated time should map to 0, got %d", got.GetUpdatedAtUnixMs())
	}
}

func TestResultToProtoFull(t *testing.T) {
	ts := time.Date(2026, 5, 10, 0, 0, 0, 0, time.UTC)
	result := backtest.Result{
		RunID:        "run-1",
		StartingCash: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1000000"}},
		EndingEquity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1100000"}},
		Trades: []backtest.TradeSummary{{
			InstrumentID: "005930",
			Side:         backtest.OrderSideBuy,
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "10"}},
			Price:        market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "70000"}},
			Timestamp:    ts,
		}},
		Positions: []backtest.PositionSummary{{
			InstrumentID: "005930",
			Quantity:     market.Quantity{Amount: market.Decimal{Value: "10"}},
			LastPrice:    market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "71000"}},
		}},
		Summary: backtest.SummaryMetrics{
			StartingCash: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1000000"}},
			EndingEquity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1100000"}},
			TotalReturn:  market.Decimal{Value: "0.1"},
			TradeCount:   1,
		},
		EquityCurve: []backtest.EquityPoint{{
			Timestamp: ts,
			Equity:    market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "1050000"}},
		}},
	}

	got := resultToProto(result)
	if got.GetRunId() != "run-1" {
		t.Errorf("run id mismatch: %q", got.GetRunId())
	}
	if got.GetStartingCash().GetAmount().GetValue() != "1000000" {
		t.Errorf("starting cash mismatch: %q", got.GetStartingCash().GetAmount().GetValue())
	}
	if got.GetStartingCash().GetCurrency() != altv1.Currency_CURRENCY_KRW {
		t.Errorf("currency mismatch: %v", got.GetStartingCash().GetCurrency())
	}
	if len(got.GetTrades()) != 1 || got.GetTrades()[0].GetSide() != "buy" {
		t.Errorf("trade mapping mismatch: %+v", got.GetTrades())
	}
	if got.GetTrades()[0].GetTimestampUnixMs() != ts.UnixMilli() {
		t.Errorf("trade timestamp mismatch: %d", got.GetTrades()[0].GetTimestampUnixMs())
	}
	if len(got.GetPositions()) != 1 {
		t.Errorf("position mapping mismatch: %+v", got.GetPositions())
	}
	if got.GetSummary().GetTradeCount() != 1 {
		t.Errorf("summary trade count mismatch: %d", got.GetSummary().GetTradeCount())
	}
	if got.GetSummary().GetTotalReturn().GetValue() != "0.1" {
		t.Errorf("summary total return mismatch: %q", got.GetSummary().GetTotalReturn().GetValue())
	}
	if len(got.GetEquityCurve()) != 1 {
		t.Errorf("equity curve mapping mismatch: %+v", got.GetEquityCurve())
	}
}

func TestResultToProtoEmptyCollections(t *testing.T) {
	got := resultToProto(backtest.Result{RunID: "run-1"})
	if got.GetTrades() != nil {
		t.Errorf("expected nil trades for empty result, got %+v", got.GetTrades())
	}
	if got.GetPositions() != nil {
		t.Errorf("expected nil positions for empty result, got %+v", got.GetPositions())
	}
	if got.GetEquityCurve() != nil {
		t.Errorf("expected nil equity curve for empty result, got %+v", got.GetEquityCurve())
	}
	// Summary is always present so callers do not nil-check it.
	if got.GetSummary() == nil {
		t.Error("expected summary to be present even for empty result")
	}
}
