package operator

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

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

func submittedLiveOrder(id string) *altv1.LiveOrder {
	return &altv1.LiveOrder{
		Id:           id,
		AccountId:    "live-acct-1",
		InstrumentId: "KRX:005930",
		Side:         "buy",
		Type:         "kis_best_limit",
		Status:       "submitted",
		BrokerId:     "broker-lo-1",
		BrokerStatus: "SUBMITTED",
	}
}

func canceledLiveOrder(id string) *altv1.LiveOrder {
	o := submittedLiveOrder(id)
	o.Status = "canceled"
	o.BrokerStatus = "CANCELED"
	return o
}

func TestRunSubmitLiveOrderConfirmationRequiredTypedError(t *testing.T) {
	api := &fakeAPI{submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{
		Error: &altv1.ErrorInfo{Code: "invalid_request", Message: "operator confirmation is required"},
	}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_order_lifecycle",
		Steps: []Step{
			{
				ID:     "submit",
				Action: ActionSubmitLiveOrder,
				Request: Request{
					AccountID:         "live-acct-1",
					InstrumentID:      "KRX:005930",
					Side:              "buy",
					Quantity:          "1",
					OrderType:         "kis_best_limit",
					OperatorConfirmed: false,
				},
				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 TestRunSubmitLiveOrderPreservesCustomOrderType(t *testing.T) {
	order := submittedLiveOrder("lo-live-acct-1-1")
	order.Type = "kis_best_limit"
	api := &fakeAPI{submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{Order: order}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_order_lifecycle",
		Steps: []Step{
			{
				ID:     "submit",
				Action: ActionSubmitLiveOrder,
				Request: Request{
					AccountID:         "live-acct-1",
					InstrumentID:      "KRX:005930",
					Side:              "buy",
					Quantity:          "1",
					OrderType:         "kis_best_limit",
					OperatorConfirmed: true,
					OperatorID:        "operator-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, "live_order_id=lo-live-acct-1-1") {
		t.Errorf("output %q missing live_order_id=lo-live-acct-1-1", out)
	}
	got := api.lastSubmitLiveOrderReq()
	if got == nil {
		t.Fatal("server did not receive a submit live order request")
	}
	if got.GetIntent().GetType() != "kis_best_limit" {
		t.Errorf("server received order type = %q, want kis_best_limit", got.GetIntent().GetType())
	}
	if !got.GetOperatorConfirmation().GetConfirmed() {
		t.Error("server received confirmed = false, want true")
	}
}

func TestRunSubmitLiveOrderCancelGetInterpolation(t *testing.T) {
	orderID := "lo-live-acct-1-1"
	api := &fakeAPI{
		submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{Order: submittedLiveOrder(orderID)},
		getLiveOrderResp:    &altv1.GetLiveOrderResponse{Order: submittedLiveOrder(orderID)},
		cancelLiveOrderResp: &altv1.CancelLiveOrderResponse{Order: canceledLiveOrder(orderID)},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_order_lifecycle",
		Steps: []Step{
			{
				ID:     "submit",
				Action: ActionSubmitLiveOrder,
				Request: Request{
					AccountID:         "live-acct-1",
					InstrumentID:      "KRX:005930",
					Side:              "buy",
					Quantity:          "1",
					OrderType:         "kis_best_limit",
					OperatorConfirmed: true,
					OperatorID:        "operator-1",
				},
				Expect: Expect{Status: "ok", LiveOrderStatus: "submitted"},
			},
			{
				ID:     "get",
				Action: ActionGetLiveOrder,
				Request: Request{
					AccountID:   "live-acct-1",
					LiveOrderID: "{{steps.submit.live_order.id}}",
				},
				Expect: Expect{Status: "ok"},
			},
			{
				ID:     "cancel",
				Action: ActionCancelLiveOrder,
				Request: Request{
					AccountID:   "live-acct-1",
					LiveOrderID: "{{steps.submit.live_order.id}}",
				},
				Expect: Expect{Status: "ok", LiveOrderStatus: "canceled"},
			},
		},
	}

	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_live_order status=ok live_order_id=lo-live-acct-1-1 live_order_status=submitted",
		"step=get action=get_live_order status=ok live_order_id=lo-live-acct-1-1",
		"step=cancel action=cancel_live_order status=ok live_order_id=lo-live-acct-1-1 live_order_status=canceled",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("output %q missing %q", out, want)
		}
	}

	// cancel step must receive the interpolated order id, not the raw placeholder
	got := api.lastCancelLiveOrderReq()
	if got == nil {
		t.Fatal("server did not receive a cancel live order request")
	}
	if got.GetLiveOrderId() != orderID {
		t.Errorf("cancel received live_order_id = %q, want interpolated %q", got.GetLiveOrderId(), orderID)
	}
}

func TestRunGetLiveRiskPolicy(t *testing.T) {
	api := &fakeAPI{
		getLiveRiskPolicyResp: &altv1.GetLiveRiskPolicyResponse{
			Policy: &altv1.LiveRiskPolicy{MaxDailyOrders: 5, MaxOpenOrders: 3, AllowShortSelling: false},
		},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_risk_kill_switch",
		Steps: []Step{
			{
				ID:      "get_policy",
				Action:  ActionGetLiveRiskPolicy,
				Request: Request{AccountID: "live-acct-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, "max_daily_orders=5") {
		t.Errorf("output %q missing max_daily_orders=5", out)
	}
	if !strings.Contains(out, "max_open_orders=3") {
		t.Errorf("output %q missing max_open_orders=3", out)
	}
	if !strings.Contains(out, "allow_short_selling=false") {
		t.Errorf("output %q missing allow_short_selling=false", out)
	}
}

func TestRunGetLiveKillSwitch(t *testing.T) {
	api := &fakeAPI{
		getLiveKillSwitchResp: &altv1.GetLiveKillSwitchResponse{
			State: &altv1.LiveKillSwitchState{Halted: true, Reason: "kill switch active by default"},
		},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_risk_kill_switch",
		Steps: []Step{
			{
				ID:      "get_ks",
				Action:  ActionGetLiveKillSwitch,
				Request: Request{AccountID: "live-acct-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, "kill_switch_halted=true") {
		t.Errorf("output %q missing kill_switch_halted=true", out)
	}
}

func TestRunSetLiveKillSwitchResume(t *testing.T) {
	api := &fakeAPI{
		setLiveKillSwitchResp: &altv1.SetLiveKillSwitchResponse{
			State: &altv1.LiveKillSwitchState{Halted: false, Reason: "operator cleared"},
		},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_risk_kill_switch",
		Steps: []Step{
			{
				ID:     "set_ks",
				Action: ActionSetLiveKillSwitch,
				Request: Request{
					AccountID:        "live-acct-1",
					Halted:           false,
					KillSwitchReason: "operator cleared",
				},
				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, "kill_switch_halted=false") {
		t.Errorf("output %q missing kill_switch_halted=false", out)
	}

	got := api.lastSetLiveKillSwitchReq()
	if got == nil {
		t.Fatal("server did not receive a set_live_kill_switch request")
	}
	if got.GetHalted() != false {
		t.Errorf("server received halted = true, want false")
	}
	if got.GetReason() != "operator cleared" {
		t.Errorf("server received reason = %q, want %q", got.GetReason(), "operator cleared")
	}
}

func TestLiveRiskKillSwitchFixtureIsValid(t *testing.T) {
	yamlPath := filepath.Join("..", "..", "testdata", "operator", "live_risk_kill_switch.yaml")
	if _, err := LoadScenario(yamlPath); err != nil {
		t.Fatalf("live_risk_kill_switch.yaml failed validation: %v", err)
	}

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

	scanner := bufio.NewScanner(f)
	sawPolicy := false
	sawKillSwitch := false
	sawRiskDecision := 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["action"] == "get_live_risk_policy" && rec["max_daily_orders"] != nil {
			sawPolicy = true
		}
		if rec["kill_switch_halted"] != nil {
			sawKillSwitch = true
		}
		if rec["risk_decision_allowed"] != nil {
			sawRiskDecision = true
		}
		if rec["type"] == "summary" {
			sawSummary = true
		}
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("scan expected fixture: %v", err)
	}
	if !sawPolicy {
		t.Error("expected fixture missing a get_live_risk_policy line with max_daily_orders")
	}
	if !sawKillSwitch {
		t.Error("expected fixture missing a kill_switch_halted field")
	}
	if !sawRiskDecision {
		t.Error("expected fixture missing a risk_decision_allowed field (risk-blocked submit evidence)")
	}
	if !sawSummary {
		t.Error("expected fixture missing a summary line")
	}
}

func TestRunSubmitLiveOrderRiskBlockedIncludesDecision(t *testing.T) {
	api := &fakeAPI{submitLiveOrderResp: &altv1.SubmitLiveOrderResponse{
		Order: &altv1.LiveOrder{
			Id:       "lo-live-acct-1-1",
			Status:   "rejected",
			BrokerId: "",
		},
		Error:        &altv1.ErrorInfo{Code: "invalid_request", Message: "live order blocked by risk policy: kill switch is active"},
		RiskDecision: &altv1.LiveRiskDecision{Allowed: false, Reason: "kill switch is active"},
	}}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_risk_kill_switch",
		Steps: []Step{
			{
				ID:     "submit_risk_blocked",
				Action: ActionSubmitLiveOrder,
				Request: Request{
					AccountID:         "live-acct-1",
					InstrumentID:      "KRX:005930",
					Side:              "buy",
					Quantity:          "1",
					OrderType:         "market",
					OperatorConfirmed: true,
					OperatorID:        "operator-1",
				},
				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, "live_order_id=lo-live-acct-1-1") {
		t.Errorf("output %q missing live_order_id=lo-live-acct-1-1", out)
	}
	if !strings.Contains(out, "live_order_status=rejected") {
		t.Errorf("output %q missing live_order_status=rejected", out)
	}
	if !strings.Contains(out, "risk_decision_allowed=false") {
		t.Errorf("output %q missing risk_decision_allowed=false", out)
	}
	if !strings.Contains(out, "kill switch is active") {
		t.Errorf("output %q missing kill switch reason", out)
	}
}

func TestLiveOrderLifecycleFixtureIsValid(t *testing.T) {
	yamlPath := filepath.Join("..", "..", "testdata", "operator", "live_order_lifecycle.yaml")
	if _, err := LoadScenario(yamlPath); err != nil {
		t.Fatalf("live_order_lifecycle.yaml failed validation: %v", err)
	}

	expectedPath := filepath.Join("..", "..", "testdata", "operator", "expected", "live_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)
	sawLiveOrderID := false
	sawCanceled := 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["live_order_id"] != nil {
			sawLiveOrderID = true
		}
		if rec["live_order_status"] == "canceled" {
			sawCanceled = true
		}
		if rec["type"] == "summary" {
			sawSummary = true
		}
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("scan expected fixture: %v", err)
	}
	if !sawLiveOrderID {
		t.Error("expected fixture missing a live_order_id line")
	}
	if !sawCanceled {
		t.Error("expected fixture missing a live_order_status=canceled transition")
	}
	if !sawSummary {
		t.Error("expected fixture missing a summary line")
	}
}

func TestRunSyncLiveAccount(t *testing.T) {
	snap := &altv1.LiveAccountSnapshot{
		AccountId:      "op-alias-001",
		Broker:         "kis",
		SyncedAtUnixMs: 1749340800000,
		Cash: []*altv1.LiveCashBalance{
			{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "5000000"}},
		},
	}
	api := &fakeAPI{
		syncLiveAccountResp: &altv1.SyncLiveAccountResponse{Snapshot: snap},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_account_sync_test",
		Steps: []Step{
			{
				ID:      "sync",
				Action:  ActionSyncLiveAccount,
				Request: Request{AccountID: "op-alias-001"},
				Expect:  Expect{Status: "ok"},
			},
		},
	}
	out, code := runScenario(t, sc, url)
	if code != 0 {
		t.Fatalf("expected exit 0, got %d\noutput:\n%s", code, out)
	}
	req := api.lastSyncLiveAccountReq()
	if req == nil {
		t.Fatal("expected server to receive SyncLiveAccountRequest")
	}
	if req.GetAccountId() != "op-alias-001" {
		t.Errorf("account_id mismatch: %q", req.GetAccountId())
	}
}

func TestRunGetLiveAccountSnapshot(t *testing.T) {
	snap := &altv1.LiveAccountSnapshot{
		AccountId:      "op-alias-001",
		Broker:         "kis",
		SyncedAtUnixMs: 1749340800000,
	}
	api := &fakeAPI{
		getLiveAccountSnapResp: &altv1.GetLiveAccountSnapshotResponse{Snapshot: snap},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_account_get_test",
		Steps: []Step{
			{
				ID:      "get",
				Action:  ActionGetLiveAccountSnapshot,
				Request: Request{AccountID: "op-alias-001"},
				Expect:  Expect{Status: "ok"},
			},
		},
	}
	out, code := runScenario(t, sc, url)
	if code != 0 {
		t.Fatalf("expected exit 0, got %d\noutput:\n%s", code, out)
	}
	req := api.lastGetLiveAccountSnapReq()
	if req == nil {
		t.Fatal("expected server to receive GetLiveAccountSnapshotRequest")
	}
	if req.GetAccountId() != "op-alias-001" {
		t.Errorf("account_id mismatch: %q", req.GetAccountId())
	}
}

func TestRunSyncLiveAccountValidationRejectsMissingAccountID(t *testing.T) {
	sc := Scenario{
		Name: "live_account_missing_id",
		Steps: []Step{
			{
				ID:     "sync",
				Action: ActionSyncLiveAccount,
			},
		},
	}
	err := sc.Validate()
	if err == nil {
		t.Fatal("expected validation error for missing account_id")
	}
}

func TestRunGetLiveAccountSnapshotValidationRejectsMissingAccountID(t *testing.T) {
	sc := Scenario{
		Name: "live_account_snap_missing_id",
		Steps: []Step{
			{
				ID:     "get",
				Action: ActionGetLiveAccountSnapshot,
			},
		},
	}
	err := sc.Validate()
	if err == nil {
		t.Fatal("expected validation error for missing account_id")
	}
}

func TestRunListLiveAuditEvents(t *testing.T) {
	api := &fakeAPI{
		listLiveAuditEventsResp: &altv1.ListLiveAuditEventsResponse{
			Events: []*altv1.LiveAuditEvent{
				{EventId: "aud-1", Type: "submit_confirmed", AccountId: "op-audit-test-001", OrderId: "lo-1"},
			},
		},
	}
	url := startFakeAPIServer(t, api)
	sc := &Scenario{
		Name: "live_audit_query",
		Steps: []Step{
			{
				ID:      "list_audit",
				Action:  ActionListLiveAuditEvents,
				Request: Request{AccountID: "op-audit-test-001"},
				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, "audit_event_count=1") {
		t.Errorf("output %q missing audit_event_count=1", out)
	}
	req := api.lastListLiveAuditEventsReq()
	if req == nil {
		t.Fatal("server did not receive ListLiveAuditEventsRequest")
	}
	if req.GetAccountId() != "op-audit-test-001" {
		t.Errorf("account_id mismatch: %q", req.GetAccountId())
	}
}

func TestLiveAuditQueryFixtureIsValid(t *testing.T) {
	yamlPath := filepath.Join("..", "..", "testdata", "operator", "live_audit_query.yaml")
	if _, err := LoadScenario(yamlPath); err != nil {
		t.Fatalf("live_audit_query.yaml failed validation: %v", err)
	}

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

	scanner := bufio.NewScanner(f)
	sawAuditCount := 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["action"] == "list_live_audit_events" && rec["audit_event_count"] != nil {
			sawAuditCount = true
		}
		if rec["type"] == "summary" {
			sawSummary = true
		}
	}
	if err := scanner.Err(); err != nil {
		t.Fatalf("scan expected fixture: %v", err)
	}
	if !sawAuditCount {
		t.Error("expected fixture missing a list_live_audit_events line with audit_event_count")
	}
	if !sawSummary {
		t.Error("expected fixture missing a summary line")
	}
}

func TestRunListLiveAuditEventsValidationRejectsMissingAccountID(t *testing.T) {
	sc := Scenario{
		Name: "live_audit_missing_id",
		Steps: []Step{
			{
				ID:     "list",
				Action: ActionListLiveAuditEvents,
			},
		},
	}
	err := sc.Validate()
	if err == nil {
		t.Fatal("expected validation error for missing account_id")
	}
}

func TestEvaluateLiveAccountTypedError(t *testing.T) {
	ev := StepEvent{Action: string(ActionSyncLiveAccount)}
	step := Step{Expect: Expect{Status: "ok"}}
	errInfo := &altv1.ErrorInfo{Code: "not_found", Message: "snapshot not found"}
	ev2, code := evaluateLiveAccount(ev, step, errInfo, nil)
	if code == codeOK {
		t.Error("expected non-OK code for typed error")
	}
	if ev2.ErrorCode != "not_found" {
		t.Errorf("error_code mismatch: %q", ev2.ErrorCode)
	}
}

func TestEvaluateLiveAccountExpectedError(t *testing.T) {
	ev := StepEvent{Action: string(ActionGetLiveAccountSnapshot)}
	step := Step{Expect: Expect{Status: "error", ErrorCode: "not_found"}}
	errInfo := &altv1.ErrorInfo{Code: "not_found", Message: "snapshot not found"}
	ev2, code := evaluateLiveAccount(ev, step, errInfo, nil)
	if code != codeOK {
		t.Errorf("expected codeOK for matched typed error, got %d", code)
	}
	if ev2.Status != statusOK {
		t.Errorf("expected status ok, got %q", ev2.Status)
	}
}
