package opsconsole

import (
	"bytes"
	"context"
	"net"
	"strings"
	"testing"
	"time"

	toki "git.toki-labs.com/toki/proto-socket/go"
	"google.golang.org/protobuf/proto"

	edgeevents "iop/apps/edge/internal/events"
	edgenode "iop/apps/edge/internal/node"
	edgeservice "iop/apps/edge/internal/service"
	iop "iop/proto/gen/iop"
)

func TestParseCommand(t *testing.T) {
	cases := []struct {
		name  string
		input string
		want  Command
	}{
		{"empty", "", Command{Kind: CommandEmpty}},
		{"whitespace", "   ", Command{Kind: CommandEmpty}},
		{"slash exit", "/exit", Command{Kind: CommandExit}},
		{"slash quit", "/quit", Command{Kind: CommandExit}},
		{"bare exit", "exit", Command{Kind: CommandExit}},
		{"bare quit upper", "QUIT", Command{Kind: CommandExit}},
		{"nodes", "/nodes", Command{Kind: CommandNodes}},
		{"capabilities", "/capabilities", Command{Kind: CommandCapabilities}},
		{"transport", "/transport", Command{Kind: CommandTransportStatus}},
		{"node with arg", "/node alias-1", Command{Kind: CommandSetNode, Arg: "alias-1"}},
		{"node missing arg", "/node ", Command{Kind: CommandSetNode, UsageErr: "usage: /node <id|alias>"}},
		{"session with arg", "/session work-1", Command{Kind: CommandSetSession, Arg: "work-1"}},
		{"session missing arg", "/session ", Command{Kind: CommandSetSession, UsageErr: "usage: /session <id>"}},
		{"background on", "/background on", Command{Kind: CommandSetBackground, Arg: "on"}},
		{"background off", "/background OFF", Command{Kind: CommandSetBackground, Arg: "off"}},
		{"background invalid", "/background maybe", Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}},
		{"background missing", "/background ", Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}},
		{"prompt", "hello world", Command{Kind: CommandPrompt, Arg: "hello world"}},
		{"prompt slash unknown is prompt? no it stays prompt", "/unknown stuff", Command{Kind: CommandPrompt, Arg: "/unknown stuff"}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := ParseCommand(tc.input)
			if got != tc.want {
				t.Fatalf("ParseCommand(%q) = %+v, want %+v", tc.input, got, tc.want)
			}
		})
	}
}

func TestConsoleStateSessionBackgroundNode(t *testing.T) {
	state := &TargetState{
		Adapter:    "ollama",
		Target:     "model",
		SessionID:  "default",
		Background: false,
		TimeoutSec: 30,
	}

	apply := func(line string) {
		t.Helper()
		cmd := ParseCommand(line)
		switch cmd.Kind {
		case CommandSetNode:
			if cmd.UsageErr != "" {
				t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr)
			}
			state.NodeRef = cmd.Arg
		case CommandSetSession:
			if cmd.UsageErr != "" {
				t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr)
			}
			state.SessionID = cmd.Arg
		case CommandSetBackground:
			if cmd.UsageErr != "" {
				t.Fatalf("unexpected usage error for %q: %s", line, cmd.UsageErr)
			}
			state.Background = cmd.Arg == "on"
		default:
			t.Fatalf("unexpected command kind %v for %q", cmd.Kind, line)
		}
	}

	apply("/node node-7")
	apply("/session worker-1")
	apply("/background on")

	if state.NodeRef != "node-7" {
		t.Errorf("NodeRef: got %q want node-7", state.NodeRef)
	}
	if state.SessionID != "worker-1" {
		t.Errorf("SessionID: got %q want worker-1", state.SessionID)
	}
	if !state.Background {
		t.Error("Background: expected true")
	}

	apply("/background off")
	if state.Background {
		t.Error("Background: expected false after /background off")
	}
}

func TestNormalizeSessionID(t *testing.T) {
	if got := NormalizeSessionID(""); got != "default" {
		t.Errorf("empty: got %q want %q", got, "default")
	}
	if got := NormalizeSessionID("my-session"); got != "my-session" {
		t.Errorf("non-empty: got %q want %q", got, "my-session")
	}
}

func TestPrintNodes_ShowsSelectedNode(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
	reg.Register(&edgenode.NodeEntry{NodeID: "node-2", Alias: "alias-2"})
	svc := edgeservice.New(reg, nil)

	var out bytes.Buffer
	PrintNodes(&out, svc.ListNodeSnapshots(), "alias-1")

	got := out.String()
	if !strings.Contains(got, "* node0 = node-1 (alias-1)") {
		t.Errorf("expected selected node-1 to have marker, got:\n%s", got)
	}
	if !strings.Contains(got, "  node1 = node-2 (alias-2)") {
		t.Errorf("expected non-selected node-2 to have no marker, got:\n%s", got)
	}
}

func TestPrintNodes_AcceptsSnapshotDTO(t *testing.T) {
	var out bytes.Buffer
	PrintNodes(&out, []edgeservice.NodeSnapshot{
		{NodeID: "node-a", Alias: "alpha", Label: "node0"},
	}, "alpha")

	got := out.String()
	if !strings.Contains(got, "* node0 = node-a (alpha)") {
		t.Errorf("expected DTO render to mark selected, got:\n%s", got)
	}
}

func TestSendRun_SubmitRunRequest_MetadataSource(t *testing.T) {
	// Create net.Pipe for client-server communication
	edgeConn, nodeConn := net.Pipe()
	defer edgeConn.Close()
	defer nodeConn.Close()

	parserMap := toki.ParserMap{
		toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
			m := &iop.RunRequest{}
			return m, proto.Unmarshal(b, m)
		},
	}

	edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
	nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)

	reqReceived := make(chan *iop.RunRequest, 1)

	toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
		reqReceived <- req
	})

	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{
		NodeID: "node-console-test",
		Alias:  "alpha",
		Client: edgeClient,
	})

	// Enable queue manager in service by passing a new Bus
	bus := edgeevents.NewBus()
	svc := edgeservice.New(reg, bus)

	target := &TargetState{
		NodeRef:    "alpha",
		Adapter:    "ollama",
		Target:     "model",
		SessionID:  "sess-1",
		Background: true,
		TimeoutSec: 10,
	}

	var out bytes.Buffer
	err := SendRun(context.Background(), svc, nil, &out, target, "test prompt")
	if err != nil {
		t.Fatalf("SendRun failed: %v", err)
	}

	select {
	case req := <-reqReceived:
		if req.GetAdapter() != "ollama" || req.GetTarget() != "model" {
			t.Errorf("unexpected run request: %+v", req)
		}
		if req.GetMetadata()["source"] != "edge-ops-console" {
			t.Errorf("expected source edge-ops-console, got %q", req.GetMetadata()["source"])
		}
		// If SendRun had set a non-empty ModelGroupKey, it would have routed through the queue.
		// Since we configured CLI.Enabled = false, resolveQueueCandidates would return an error
		// ("node ... does not support adapter ..."), causing SendRun to fail.
		// Successful delivery confirms that SendRun maintains ModelGroupKey == "" (direct path).
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for RunRequest at node")
	}
}

func TestWriteReasoningDeltaPrefixesEachLine(t *testing.T) {
	var out bytes.Buffer
	writeReasoningDelta(&out, "node0", "사용자가 인사했습니다.\n\x1f\x1f\n")
	got := out.String()
	want1 := "[node0-reasoning] 사용자가 인사했습니다."
	want2 := "[node0-reasoning] \x1f\x1f"
	if !strings.Contains(got, want1) {
		t.Errorf("expected %q, got:\n%s", want1, got)
	}
	if !strings.Contains(got, want2) {
		t.Errorf("expected %q, got:\n%s", want2, got)
	}
}

func TestWriteReasoningDeltaIgnoresEmptyDelta(t *testing.T) {
	var out bytes.Buffer
	writeReasoningDelta(&out, "node0", "")
	if out.Len() != 0 {
		t.Errorf("expected no output for empty delta, got:\n%s", out.String())
	}
}

func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) {
	view := edgeservice.NodeCommandView{
		NodeLabel: "node0",
		Adapter:   "cli@local",
		Target:    "codex",
		SessionID: "default",
		Result: map[string]string{
			"targets": "v1,v2",
			"version": "1.0",
		},
	}
	var out bytes.Buffer
	FormatNodeCommandView(&out, "capabilities", view)
	got := out.String()
	if !strings.Contains(got, "[node0-capabilities] adapter=cli@local target=codex session=default") {
		t.Errorf("missing header: %q", got)
	}
	// keys must appear in sorted order: targets before version
	tIdx := strings.Index(got, "targets = v1,v2")
	vIdx := strings.Index(got, "version = 1.0")
	if tIdx < 0 || vIdx < 0 || tIdx > vIdx {
		t.Errorf("expected sorted keys, got: %q", got)
	}
}

func TestFormatNodeCommandView_EmptyResult(t *testing.T) {
	view := edgeservice.NodeCommandView{NodeLabel: "alpha", Target: "codex", SessionID: "default"}
	var out bytes.Buffer
	FormatNodeCommandView(&out, "sessions", view)
	if !strings.Contains(out.String(), "no sessions payload returned") {
		t.Errorf("expected empty-payload notice, got: %q", out.String())
	}
}

func TestFormatNodeCommandView_RendersTransportStatusKeys(t *testing.T) {
	view := edgeservice.NodeCommandView{
		NodeLabel: "node0",
		Adapter:   "cli@local",
		Target:    "claude",
		SessionID: "default",
		Result: map[string]string{
			"connected":  "true",
			"state":      "connected",
			"node_id":    "test-node",
			"session_id": "default",
			"adapter":    "cli",
			"target":     "claude",
		},
	}
	var out bytes.Buffer
	FormatNodeCommandView(&out, "transport", view)
	got := out.String()
	if !strings.Contains(got, "[node0-transport] adapter=cli@local target=claude session=default") {
		t.Errorf("missing header: %q", got)
	}
	for _, want := range []string{"connected = true", "state = connected", "node_id = test-node", "session_id = default"} {
		if !strings.Contains(got, want) {
			t.Errorf("expected %q in transport output, got:\n%s", want, got)
		}
	}
}
