package operator

import (
	"bufio"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

// TestPaperTradingFixtureIsValid guards the paper trading scenario fixture and
// its expected output: the YAML must pass dry-run validation and every expected
// JSONL line must parse and carry the paper account_id key the operator relies
// on for monitoring.
func TestPaperTradingFixtureIsValid(t *testing.T) {
	yamlPath := filepath.Join("..", "..", "testdata", "operator", "paper_trading_state.yaml")
	if _, err := LoadScenario(yamlPath); err != nil {
		t.Fatalf("paper_trading_state.yaml failed validation: %v", err)
	}

	expectedPath := filepath.Join("..", "..", "testdata", "operator", "expected", "paper_trading_state.jsonl")
	f, err := os.Open(expectedPath)
	if err != nil {
		t.Fatalf("open expected fixture: %v", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	sawAccount := false
	sawSummary := false
	sawEquity := false
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		var rec map[string]any
		if err := json.Unmarshal([]byte(line), &rec); err != nil {
			t.Fatalf("expected fixture line not valid JSON: %v", err)
		}
		if rec["scenario"] != "paper_trading_state" {
			t.Errorf("line scenario = %v, want paper_trading_state", rec["scenario"])
		}
		if rec["account_id"] == "paper-1" {
			sawAccount = true
			if _, ok := rec["equity_point_count"]; ok {
				if _, ok := rec["latest_equity"]; ok {
					sawEquity = true
				}
			}
		}
		if rec["type"] == "summary" {
			sawSummary = true
		}
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("scan expected fixture: %v", err)
	}
	if !sawAccount {
		t.Error("expected fixture missing an account_id=paper-1 line")
	}
	if !sawSummary {
		t.Error("expected fixture missing a summary line")
	}
	if !sawEquity {
		t.Error("expected fixture missing equity_point_count/latest_equity on a paper line")
	}
}

func paperState(accountID string) *altv1.PaperTradingState {
	return &altv1.PaperTradingState{
		AccountId: accountID,
		Run: &altv1.BacktestRun{
			Id:     "paper-run-1",
			Status: altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_SUCCEEDED,
		},
		Cash: &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "9998950"}},
		Positions: []*altv1.BacktestPosition{
			{InstrumentId: "KRX:005930", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}, LastPrice: &altv1.Price{Amount: &altv1.Decimal{Value: "1150"}}},
		},
		Fills: []*altv1.BacktestTrade{
			{InstrumentId: "KRX:005930", Side: "buy", Price: &altv1.Price{Amount: &altv1.Decimal{Value: "1050"}}},
		},
		EquityCurve: []*altv1.BacktestEquityPoint{
			{TimestampUnixMs: 1746057600000, Equity: &altv1.Price{Amount: &altv1.Decimal{Value: "10000000"}}},
			{TimestampUnixMs: 1747267200000, Equity: &altv1.Price{Amount: &altv1.Decimal{Value: "10000100"}}},
		},
	}
}

func TestRunStartPaperTradingOutputsAccountState(t *testing.T) {
	api := &fakeAPI{startPaperResp: &altv1.StartPaperTradingResponse{State: paperState("paper-1")}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_trading_state",
		Steps: []Step{
			{
				ID:     "start",
				Action: ActionStartPaperTrading,
				Request: Request{
					AccountID:    "paper-1",
					StrategyID:   "strategy-v1",
					Market:       "kr",
					Timeframe:    "daily",
					FromUnixMs:   1746057600000,
					ToUnixMs:     1747267200000,
					StartingCash: "10000000",
				},
				Expect: Expect{Status: "ok", RunStatus: "succeeded"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	for _, want := range []string{
		"account_id=paper-1",
		"run.status=succeeded",
		"cash=9998950",
		"position_count=1",
		"fill_count=1",
		"risk=clear",
		"equity_point_count=2",
		"latest_equity=10000100",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("output %q missing %q", out, want)
		}
	}

	got := api.lastStartPaperReq()
	if got == nil {
		t.Fatal("server did not receive a start paper trading request")
	}
	if got.GetAccountId() != "paper-1" {
		t.Errorf("server received account_id = %q, want paper-1", got.GetAccountId())
	}
	if got.GetStartingCash().GetCurrency() != altv1.Currency_CURRENCY_KRW {
		t.Errorf("server received currency = %v, want KRW (derived from kr market)", got.GetStartingCash().GetCurrency())
	}
	if got.GetStartingCash().GetAmount().GetValue() != "10000000" {
		t.Errorf("server received starting cash = %q, want 10000000", got.GetStartingCash().GetAmount().GetValue())
	}
}

func TestRunGetPaperTradingStateOutputsFields(t *testing.T) {
	api := &fakeAPI{getPaperStateResp: &altv1.GetPaperTradingStateResponse{State: paperState("paper-1")}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_trading_state",
		Steps: []Step{
			{
				ID:      "state",
				Action:  ActionGetPaperTradingState,
				Request: Request{AccountID: "paper-1"},
				Expect:  Expect{Status: "ok"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	if !strings.Contains(out, "account_id=paper-1") || !strings.Contains(out, "position_count=1") || !strings.Contains(out, "risk=clear") {
		t.Errorf("output %q missing paper state fields", out)
	}
	if !strings.Contains(out, "equity_point_count=2") || !strings.Contains(out, "latest_equity=10000100") {
		t.Errorf("output %q missing equity summary fields", out)
	}

	got := api.lastGetPaperStateReq()
	if got == nil {
		t.Fatal("server did not receive a get paper state request")
	}
	if got.GetAccountId() != "paper-1" {
		t.Errorf("server received account_id = %q, want paper-1", got.GetAccountId())
	}
}

func TestRunGetPaperTradingStateNotFoundTypedError(t *testing.T) {
	api := &fakeAPI{getPaperStateResp: &altv1.GetPaperTradingStateResponse{
		Error: &altv1.ErrorInfo{Code: "not_found", Message: "paper trading account not found"},
	}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_trading_state",
		Steps: []Step{
			{
				ID:      "state",
				Action:  ActionGetPaperTradingState,
				Request: Request{AccountID: "missing"},
				Expect:  Expect{Status: "error", ErrorCode: "not_found"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	if !strings.Contains(out, "error.code=not_found") {
		t.Errorf("output %q missing error.code=not_found", out)
	}
}

// blockedPaperState mirrors paperState but carries a risk rejection so the
// operator surface reports a blocked risk summary instead of a clear one.
func blockedPaperState(accountID string) *altv1.PaperTradingState {
	state := paperState(accountID)
	state.RiskRejections = []*altv1.PaperRiskRejection{
		{
			InstrumentId:    "KRX:000660",
			Side:            "sell",
			Quantity:        &altv1.Quantity{Amount: &altv1.Decimal{Value: "2"}},
			Reason:          "insufficient position quantity for sell order",
			TimestampUnixMs: 1746230400000,
		},
	}
	return state
}

func TestRunGetPaperTradingStateOutputsRiskFields(t *testing.T) {
	api := &fakeAPI{getPaperStateResp: &altv1.GetPaperTradingStateResponse{State: blockedPaperState("paper-1")}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_trading_state",
		Steps: []Step{
			{
				ID:      "state",
				Action:  ActionGetPaperTradingState,
				Request: Request{AccountID: "paper-1"},
				Expect:  Expect{Status: "ok"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	for _, want := range []string{
		"risk=blocked",
		"risk_rejected_count=1",
		`risk_reason="insufficient position quantity for sell order"`,
	} {
		if !strings.Contains(out, want) {
			t.Errorf("output %q missing %q", out, want)
		}
	}
	if strings.Contains(out, "risk=clear") {
		t.Errorf("output %q should not report risk=clear for a blocked run", out)
	}
}

// --- order lifecycle ---

func pendingOrder(orderID string) *altv1.PaperOrder {
	return &altv1.PaperOrder{
		OrderId:      orderID,
		AccountId:    "paper-1",
		InstrumentId: "KRX:005930",
		Side:         "buy",
		Quantity:     &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}},
		Type:         "market",
		Status:       "pending",
	}
}

func filledOrder(orderID, price string) *altv1.PaperOrder {
	order := pendingOrder(orderID)
	order.Status = "filled"
	order.Fill = &altv1.BacktestTrade{
		InstrumentId: "KRX:005930",
		Side:         "buy",
		Quantity:     &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}},
		Price:        &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: price}},
	}
	return order
}

// TestRunPaperOrderLifecycle exercises submit -> fill across steps, asserting the
// order id is surfaced, the status transitions pending -> filled, the fill
// summary is emitted, and the order id is interpolated into the fill step.
func TestRunPaperOrderLifecycle(t *testing.T) {
	api := &fakeAPI{
		submitPaperOrderResp: &altv1.SubmitPaperOrderResponse{Order: pendingOrder("paper-order-paper-1-1")},
		fillPaperOrderResp: &altv1.FillPaperOrderResponse{
			Order: filledOrder("paper-order-paper-1-1", "1000"),
			State: paperState("paper-1"),
		},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_order_lifecycle",
		Steps: []Step{
			{
				ID:     "submit",
				Action: ActionSubmitPaperOrder,
				Request: Request{
					AccountID:    "paper-1",
					InstrumentID: "KRX:005930",
					Side:         "buy",
					Quantity:     "1",
					OrderType:    "market",
					Market:       "kr",
				},
				Expect: Expect{Status: "ok", OrderStatus: "pending"},
			},
			{
				ID:     "fill",
				Action: ActionFillPaperOrder,
				Request: Request{
					AccountID: "paper-1",
					OrderID:   "{{steps.submit.order.id}}",
					Market:    "kr",
					FillPrice: "1000",
				},
				Expect: Expect{Status: "ok", OrderStatus: "filled"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	for _, want := range []string{
		"step=submit action=submit_paper_order status=ok order_id=paper-order-paper-1-1 order_status=pending",
		"step=fill action=fill_paper_order status=ok order_id=paper-order-paper-1-1 order_status=filled fill_count=1 fill_price=1000",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("output %q missing %q", out, want)
		}
	}

	// The fill step must have received the order id interpolated from the submit
	// response, not the raw placeholder.
	got := api.lastFillPaperOrderReq()
	if got == nil {
		t.Fatal("server did not receive a fill paper order request")
	}
	if got.GetOrderId() != "paper-order-paper-1-1" {
		t.Errorf("fill received order_id = %q, want interpolated paper-order-paper-1-1", got.GetOrderId())
	}
	if got.GetFillPrice().GetCurrency() != altv1.Currency_CURRENCY_KRW {
		t.Errorf("fill received currency = %v, want KRW (derived from kr market)", got.GetFillPrice().GetCurrency())
	}
}

func TestRunSubmitPaperOrderLimitForwardsLimitPrice(t *testing.T) {
	api := &fakeAPI{submitPaperOrderResp: &altv1.SubmitPaperOrderResponse{Order: pendingOrder("paper-order-paper-1-1")}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_order_lifecycle",
		Steps: []Step{
			{
				ID:     "submit",
				Action: ActionSubmitPaperOrder,
				Request: Request{
					AccountID:    "paper-1",
					InstrumentID: "KRX:005930",
					Side:         "buy",
					Quantity:     "1",
					OrderType:    "limit",
					LimitPrice:   "900",
					Market:       "kr",
				},
				Expect: Expect{Status: "ok"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	got := api.lastSubmitPaperOrderReq()
	if got == nil {
		t.Fatal("server did not receive a submit paper order request")
	}
	if got.GetType() != "limit" {
		t.Errorf("submit received type = %q, want limit", got.GetType())
	}
	if got.GetLimitPrice().GetAmount().GetValue() != "900" || got.GetLimitPrice().GetCurrency() != altv1.Currency_CURRENCY_KRW {
		t.Errorf("submit received limit price = %+v, want 900 KRW", got.GetLimitPrice())
	}
}

// TestRunSubmitPaperOrderTypedError confirms the CLI surfaces a worker-returned
// typed invalid_request for an order lifecycle step (e.g. malformed quantity).
// The worker owns decimal validation; this asserts the thin CLI forward path
// reports the typed error code rather than treating it as transport failure.
func TestRunSubmitPaperOrderTypedError(t *testing.T) {
	api := &fakeAPI{submitPaperOrderResp: &altv1.SubmitPaperOrderResponse{
		Error: &altv1.ErrorInfo{Code: "invalid_request", Message: "invalid paper trading request: invalid paper order input: quantity must be greater than zero"},
	}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_order_lifecycle",
		Steps: []Step{
			{
				ID:     "submit",
				Action: ActionSubmitPaperOrder,
				Request: Request{
					AccountID:    "paper-1",
					InstrumentID: "KRX:005930",
					Side:         "buy",
					Quantity:     "0",
					OrderType:    "market",
					Market:       "kr",
				},
				Expect: Expect{Status: "error", ErrorCode: "invalid_request"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	if !strings.Contains(out, "error.code=invalid_request") {
		t.Errorf("output %q missing error.code=invalid_request", out)
	}
}

func TestRunCancelPaperOrder(t *testing.T) {
	canceled := pendingOrder("paper-order-paper-1-1")
	canceled.Status = "canceled"
	api := &fakeAPI{cancelPaperOrderResp: &altv1.CancelPaperOrderResponse{Order: canceled}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_order_lifecycle",
		Steps: []Step{
			{
				ID:      "cancel",
				Action:  ActionCancelPaperOrder,
				Request: Request{AccountID: "paper-1", OrderID: "paper-order-paper-1-1"},
				Expect:  Expect{Status: "ok", OrderStatus: "canceled"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	if !strings.Contains(out, "order_id=paper-order-paper-1-1 order_status=canceled") {
		t.Errorf("output %q missing canceled order line", out)
	}
	if strings.Contains(out, "fill_count") {
		t.Errorf("cancel output %q should not emit fill_count", out)
	}
}

// TestRunFillPaperOrderRejectedSurfacesStatus confirms a risk/portfolio denial is
// a successful response carrying order_status=rejected with fill_count=0, not a
// transport or typed error.
func TestRunFillPaperOrderRejectedSurfacesStatus(t *testing.T) {
	rejected := pendingOrder("paper-order-paper-1-1")
	rejected.Status = "rejected"
	rejected.Reason = "insufficient cash for fill"
	api := &fakeAPI{fillPaperOrderResp: &altv1.FillPaperOrderResponse{Order: rejected, State: paperState("paper-1")}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_order_lifecycle",
		Steps: []Step{
			{
				ID:      "fill",
				Action:  ActionFillPaperOrder,
				Request: Request{AccountID: "paper-1", OrderID: "paper-order-paper-1-1", Market: "kr", FillPrice: "1000"},
				Expect:  Expect{Status: "ok", OrderStatus: "rejected"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	for _, want := range []string{
		"order_status=rejected",
		"fill_count=0",
		`order_reason="insufficient cash for fill"`,
	} {
		if !strings.Contains(out, want) {
			t.Errorf("output %q missing %q", out, want)
		}
	}
}

// TestPaperOrderLifecycleFixtureIsValid guards the order lifecycle scenario
// fixture and its expected output: the YAML must pass dry-run validation and the
// expected JSONL must carry the order id and a filled status transition.
func TestPaperOrderLifecycleFixtureIsValid(t *testing.T) {
	yamlPath := filepath.Join("..", "..", "testdata", "operator", "paper_order_lifecycle.yaml")
	if _, err := LoadScenario(yamlPath); err != nil {
		t.Fatalf("paper_order_lifecycle.yaml failed validation: %v", err)
	}

	expectedPath := filepath.Join("..", "..", "testdata", "operator", "expected", "paper_order_lifecycle.jsonl")
	f, err := os.Open(expectedPath)
	if err != nil {
		t.Fatalf("open expected fixture: %v", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	sawOrderID := false
	sawFilled := false
	sawSummary := false
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		var rec map[string]any
		if err := json.Unmarshal([]byte(line), &rec); err != nil {
			t.Fatalf("expected fixture line not valid JSON: %v", err)
		}
		if rec["order_id"] != nil {
			sawOrderID = true
		}
		if rec["order_status"] == "filled" {
			sawFilled = true
		}
		if rec["type"] == "summary" {
			sawSummary = true
		}
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("scan expected fixture: %v", err)
	}
	if !sawOrderID {
		t.Error("expected fixture missing an order_id line")
	}
	if !sawFilled {
		t.Error("expected fixture missing an order_status=filled transition")
	}
	if !sawSummary {
		t.Error("expected fixture missing a summary line")
	}
}

// TestRunStartPaperTradingClearRiskOmitsRejectionFields confirms a clear run
// keeps minimal output: risk=clear with no risk_rejected_count/risk_reason keys.
func TestRunStartPaperTradingClearRiskOmitsRejectionFields(t *testing.T) {
	api := &fakeAPI{startPaperResp: &altv1.StartPaperTradingResponse{State: paperState("paper-1")}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "paper_trading_state",
		Steps: []Step{
			{
				ID:     "start",
				Action: ActionStartPaperTrading,
				Request: Request{
					AccountID:    "paper-1",
					StrategyID:   "strategy-v1",
					Market:       "kr",
					Timeframe:    "daily",
					FromUnixMs:   1746057600000,
					ToUnixMs:     1747267200000,
					StartingCash: "10000000",
				},
				Expect: Expect{Status: "ok", RunStatus: "succeeded"},
			},
		},
	}

	out, code := runScenario(t, sc, url)
	if code != codeOK {
		t.Fatalf("exit code = %d, want 0 (out=%q)", code, out)
	}
	if !strings.Contains(out, "risk=clear") {
		t.Errorf("output %q missing risk=clear", out)
	}
	if strings.Contains(out, "risk_rejected_count") || strings.Contains(out, "risk_reason") {
		t.Errorf("clear run output %q should omit risk rejection detail keys", out)
	}
}
