package opsconsole

import (
	"bytes"
	"strings"
	"testing"

	edgenode "iop/apps/edge/internal/node"
	eventpkg "iop/packages/go/events"
	iop "iop/proto/gen/iop"
)

func TestEventRouterSuppressesTrackedForegroundRun(t *testing.T) {
	var out bytes.Buffer
	router := NewEventRouter(&out, nil, nil)
	unregister := router.TrackForeground("run-1")
	defer unregister()

	router.Handle(&iop.RunEvent{RunId: "run-1", Type: "start"})

	if out.Len() != 0 {
		t.Fatalf("registered foreground event should not be printed asynchronously: %q", out.String())
	}
}

func TestEventRouterIgnoresLateForegroundAfterUntrack(t *testing.T) {
	var out bytes.Buffer
	router := NewEventRouter(&out, nil, nil)
	unregister := router.TrackForeground("run-fg")
	unregister()

	router.Handle(&iop.RunEvent{RunId: "run-fg", Type: "start", SessionId: "s1"})
	router.Handle(&iop.RunEvent{RunId: "run-fg", Type: "delta", Delta: "hi"})
	router.Handle(&iop.RunEvent{RunId: "run-fg", Type: "complete", Message: "done"})

	if got := out.String(); got != "" {
		t.Fatalf("expected no async output for late foreground events, got:\n%s", got)
	}
}

func TestEventRouterPrintsUnregisteredRun(t *testing.T) {
	var out bytes.Buffer
	router := NewEventRouter(&out, nil, nil)

	router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "start", SessionId: "s1", Background: true})
	router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: "hello", Background: true})
	if got := out.String(); !strings.Contains(got, "[unknown-msg] hello") {
		t.Fatalf("expected delta to be printed before completion, got:\n%s", got)
	}
	router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: " world\n", Background: true})
	router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "complete", Message: "done", Background: true})

	got := out.String()
	for _, want := range []string{
		"[unknown-evt] start run_id=run-bg session=s1 background=true",
		"[unknown-evt] complete run_id=run-bg detail=\"done\"",
		"[unknown-msg] hello world",
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("expected output to contain %q, got:\n%s", want, got)
		}
	}
}

func TestResponseStreamWritesBeforeFinish(t *testing.T) {
	var out bytes.Buffer
	stream := NewResponseStream(&out, "[node0-msg] ")

	stream.Write("hello")
	if got := out.String(); got != "[node0-msg] hello" {
		t.Fatalf("Write should print immediately, got %q", got)
	}

	stream.Write(" world")
	stream.Finish()
	if got := out.String(); got != "[node0-msg] hello world\n" {
		t.Fatalf("Finish should close the line, got %q", got)
	}
}

func TestResponseStreamPrefixesEachMessageLine(t *testing.T) {
	var out bytes.Buffer
	stream := NewResponseStream(&out, "[node0-msg] ")

	stream.Write("line one\nline two\nline three")
	stream.Finish()

	want := "[node0-msg] line one\n[node0-msg] line two\n[node0-msg] line three\n"
	if got := out.String(); got != want {
		t.Fatalf("unexpected multiline output:\nwant: %q\n got: %q", want, got)
	}
}

func TestEventRouterPrintsNodeScopedAsyncRun(t *testing.T) {
	var out bytes.Buffer
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
	router := NewEventRouter(&out, reg, nil)

	router.Handle(&iop.RunEvent{
		RunId:      "run-1",
		Type:       "start",
		NodeId:     "node-1",
		Background: true,
	})

	got := out.String()
	if !strings.Contains(got, "[node0-evt] start run_id=run-1") {
		t.Errorf("expected node0 event label, got:\n%s", got)
	}
}

func TestEventRouterPrefersRunEventAlias(t *testing.T) {
	var out bytes.Buffer
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "registry-alias"})
	router := NewEventRouter(&out, reg, nil)

	router.Handle(&iop.RunEvent{
		RunId:      "run-1",
		Type:       "start",
		NodeId:     "node-1",
		NodeAlias:  "event-alias",
		Background: true,
	})

	got := out.String()
	if !strings.Contains(got, "[node0-evt] start run_id=run-1") {
		t.Errorf("expected node0 event label, got:\n%s", got)
	}
}

func TestEventRouterFallsBackToNodeID(t *testing.T) {
	var out bytes.Buffer
	router := NewEventRouter(&out, nil, nil)

	router.Handle(&iop.RunEvent{
		RunId:      "run-1",
		Type:       "start",
		NodeId:     "some-raw-id",
		Background: true,
	})

	got := out.String()
	if !strings.Contains(got, "[some-raw-id-evt] start run_id=run-1") {
		t.Errorf("expected raw id event label, got:\n%s", got)
	}
}

func TestEventRouterPrintsReasoningDeltaForAsyncRun(t *testing.T) {
	var out bytes.Buffer
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "node1"})
	router := NewEventRouter(&out, reg, nil)

	router.Handle(&iop.RunEvent{
		RunId:      "run-rd",
		Type:       "reasoning_delta",
		Delta:      "사용자가 인사했습니다.\n\x1f\x1f\n",
		NodeId:     "node-1",
		Background: true,
	})

	got := out.String()
	want1 := "[node0-reasoning] 사용자가 인사했습니다."
	want2 := "[node0-reasoning] \x1f\x1f"
	if !strings.Contains(got, want1) {
		t.Errorf("expected %q in output, got:\n%s", want1, got)
	}
	if !strings.Contains(got, want2) {
		t.Errorf("expected %q in output, got:\n%s", want2, got)
	}
	if strings.Contains(got, "reasoning_delta run_id=") || strings.Contains(got, `detail=""`) {
		t.Errorf("reasoning delta should not fall through to detail, got:\n%s", got)
	}
	if strings.Contains(got, "[node0-msg]") {
		t.Errorf("reasoning delta should not appear as msg content, got:\n%s", got)
	}
}

func TestEventRouterPrintsNodeLifecycleEvents(t *testing.T) {
	var out bytes.Buffer
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
	router := NewEventRouter(&out, reg, nil)

	router.HandleNodeEvent(&iop.EdgeNodeEvent{
		Type:   eventpkg.TypeNodeDisconnected,
		NodeId: "node-1",
		Alias:  "alias-1",
		Reason: eventpkg.ReasonTransportClosed,
		Metadata: map[string]string{
			eventpkg.MetadataTransportCloseReason: "heartbeat_timeout",
			eventpkg.MetadataTransportCloseError:  "no heartbeat response within 10s",
		},
	})

	got := out.String()
	if !strings.Contains(got, `[node0-evt] disconnected reason="transport_closed"`) {
		t.Errorf("expected disconnected lifecycle output, got:\n%s", got)
	}
	if !strings.Contains(got, `transport_close_reason="heartbeat_timeout"`) {
		t.Errorf("expected transport close reason, got:\n%s", got)
	}
	if !strings.Contains(got, `transport_close_error="no heartbeat response within 10s"`) {
		t.Errorf("expected transport close error, got:\n%s", got)
	}
}

func TestEventRouterKeepsCachedDisplayLabelForLateEvents(t *testing.T) {
	var out bytes.Buffer
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
	router := NewEventRouter(&out, reg, nil)

	router.HandleNodeEvent(&iop.EdgeNodeEvent{
		Type:   eventpkg.TypeNodeConnected,
		NodeId: "node-1",
		Alias:  "alias-1",
		Reason: eventpkg.ReasonRegistered,
	})
	reg.Unregister("node-1")

	router.Handle(&iop.RunEvent{
		RunId:      "run-late",
		Type:       "start",
		NodeId:     "node-1",
		NodeAlias:  "alias-1",
		Background: true,
	})
	router.Handle(&iop.RunEvent{
		RunId:      "run-late",
		Type:       "delta",
		Delta:      "late payload",
		NodeId:     "node-1",
		NodeAlias:  "alias-1",
		Background: true,
	})
	router.Handle(&iop.RunEvent{
		RunId:      "run-late",
		Type:       "complete",
		Message:    "done",
		NodeId:     "node-1",
		NodeAlias:  "alias-1",
		Background: true,
	})

	got := out.String()
	for _, want := range []string{
		"[node0-evt] start run_id=run-late",
		"[node0-msg] late payload",
		"[node0-evt] complete run_id=run-late detail=\"done\"",
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("expected cached label output %q, got:\n%s", want, got)
		}
	}
	if strings.Contains(got, "[alias-1-") || strings.Contains(got, "[node-1-") {
		t.Fatalf("late event leaked fallback label, got:\n%s", got)
	}
}
