package transport

import (
	"context"
	"testing"

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

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

func TestEdgeParserMap_NodeCommandResponse(t *testing.T) {
	parsers := edgeParserMap()
	original := &iop.NodeCommandResponse{
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
		Adapter:   "cli",
		Target:    "codex",
		SessionId: "default",
		UsageStatus: &iop.AgentUsageStatus{
			RawOutput: "test",
		},
	}
	payload, err := proto.Marshal(original)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}

	key := toki.TypeNameOf(original)
	parser, ok := parsers[key]
	if !ok {
		t.Fatalf("parser not found for key: %s", key)
	}

	parsed, err := parser(payload)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	got := parsed.(*iop.NodeCommandResponse)
	if got.GetType() != original.GetType() ||
		got.GetAdapter() != original.GetAdapter() ||
		got.GetTarget() != original.GetTarget() ||
		got.GetSessionId() != original.GetSessionId() ||
		got.GetUsageStatus().GetRawOutput() != original.GetUsageStatus().GetRawOutput() {
		t.Fatalf("unexpected node command response: %+v", got)
	}
}

func TestEdgeParserMap_NodeCommandResponse_NewTypes(t *testing.T) {
	parsers := edgeParserMap()
	cases := []struct {
		cmdType           iop.NodeCommandType
		result            map[string]string
		providerSnapshots []*iop.ProviderSnapshot
	}{
		{
			cmdType: iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES,
			result:  map[string]string{"targets": "codex,antigravity"},
			providerSnapshots: []*iop.ProviderSnapshot{
				{
					Adapter:  "cli",
					Status:   "available",
					Capacity: 4,
					InFlight: 2,
					Queued:   0,
				},
			},
		},
		{iop.NodeCommandType_NODE_COMMAND_TYPE_SESSION_LIST, map[string]string{"count": "2"}, nil},
		{iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS, map[string]string{"connected": "true"}, nil},
	}
	for _, tc := range cases {
		original := &iop.NodeCommandResponse{
			RequestId:         "req-1",
			Type:              tc.cmdType,
			Adapter:           "cli",
			Target:            "codex",
			SessionId:         "default",
			Result:            tc.result,
			ProviderSnapshots: tc.providerSnapshots,
		}
		payload, err := proto.Marshal(original)
		if err != nil {
			t.Fatalf("marshal %v: %v", tc.cmdType, err)
		}
		parser, ok := parsers[toki.TypeNameOf(original)]
		if !ok {
			t.Fatalf("parser not found for NodeCommandResponse")
		}
		parsed, err := parser(payload)
		if err != nil {
			t.Fatalf("parse %v: %v", tc.cmdType, err)
		}
		got := parsed.(*iop.NodeCommandResponse)
		if got.GetType() != tc.cmdType {
			t.Fatalf("type: got %v want %v", got.GetType(), tc.cmdType)
		}
		for k, v := range tc.result {
			if got.GetResult()[k] != v {
				t.Fatalf("result[%q]: got %q want %q", k, got.GetResult()[k], v)
			}
		}
		if len(got.GetProviderSnapshots()) != len(tc.providerSnapshots) {
			t.Fatalf("provider snapshots count: got %d want %d", len(got.GetProviderSnapshots()), len(tc.providerSnapshots))
		}
		for i, snap := range tc.providerSnapshots {
			g := got.GetProviderSnapshots()[i]
			if g.GetAdapter() != snap.GetAdapter() ||
				g.GetStatus() != snap.GetStatus() ||
				g.GetCapacity() != snap.GetCapacity() ||
				g.GetInFlight() != snap.GetInFlight() ||
				g.GetQueued() != snap.GetQueued() {
				t.Fatalf("provider snapshot mismatch at index %d: %+v vs %+v", i, g, snap)
			}
		}
	}
}

func TestEdgeParserMap_EdgeNodeEvent(t *testing.T) {
	parsers := edgeParserMap()
	original := &iop.EdgeNodeEvent{
		Type:   "node.disconnected",
		Source: "edge",
		NodeId: "node-1",
		Alias:  "local-node",
		Reason: "transport_closed",
	}
	payload, err := proto.Marshal(original)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}

	key := toki.TypeNameOf(original)
	parser, ok := parsers[key]
	if !ok {
		t.Fatalf("parser not found for key: %s", key)
	}

	parsed, err := parser(payload)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	got := parsed.(*iop.EdgeNodeEvent)
	if got.GetType() != original.GetType() ||
		got.GetSource() != original.GetSource() ||
		got.GetNodeId() != original.GetNodeId() ||
		got.GetAlias() != original.GetAlias() ||
		got.GetReason() != original.GetReason() {
		t.Fatalf("unexpected edge node event: %+v", got)
	}
}

func TestServerEnrichesRunEventNodeAlias(t *testing.T) {
	registry := edgenode.NewRegistry()
	registry.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
	s := &Server{registry: registry}

	event := &iop.RunEvent{RunId: "run-1", Type: "start", NodeId: "node-1"}
	s.enrichRunEvent(event)
	if event.GetNodeAlias() != "alias-1" {
		t.Fatalf("expected alias-1, got %q", event.GetNodeAlias())
	}

	preset := &iop.RunEvent{RunId: "run-2", Type: "start", NodeId: "node-1", NodeAlias: "preset"}
	s.enrichRunEvent(preset)
	if preset.GetNodeAlias() != "preset" {
		t.Fatalf("expected preset to be preserved, got %q", preset.GetNodeAlias())
	}

	unknown := &iop.RunEvent{RunId: "run-3", Type: "start", NodeId: "node-x"}
	s.enrichRunEvent(unknown)
	if unknown.GetNodeAlias() != "" {
		t.Fatalf("expected empty alias for unknown node, got %q", unknown.GetNodeAlias())
	}
}

// TestEdgeParserMap_NodeConfigRefreshResponse verifies the parser map includes
// the NodeConfigRefreshResponse type so Edge can receive Node ack messages.
func TestEdgeParserMap_NodeConfigRefreshResponse(t *testing.T) {
	parsers := edgeParserMap()
	original := &iop.NodeConfigRefreshResponse{
		RequestId: "req-refresh-1",
		Status:    iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_APPLIED,
	}
	payload, err := proto.Marshal(original)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	key := toki.TypeNameOf(original)
	parser, ok := parsers[key]
	if !ok {
		t.Fatalf("parser not found for key: %s", key)
	}
	parsed, err := parser(payload)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	got := parsed.(*iop.NodeConfigRefreshResponse)
	if got.GetRequestId() != original.GetRequestId() || got.GetStatus() != original.GetStatus() {
		t.Fatalf("unexpected response: %+v", got)
	}
}

// TestServerPushConfigRefreshSkippedWhenNodeDisconnected verifies that a node
// configured in NodeStore but absent from the live registry is recorded as
// skipped. This represents the real disconnected-node path: the disconnect
// listener removes the entry from the registry, so configured-but-disconnected
// nodes never appear in registry.All().
func TestServerPushConfigRefreshSkippedWhenNodeDisconnected(t *testing.T) {
	nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
		{ID: "node-1", Alias: "alias-1", Token: "tok-1"},
	})
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}
	// Registry has no entry for node-1 (simulates post-disconnect state).
	registry := edgenode.NewRegistry()

	s := &Server{
		registry:          registry,
		nodeStore:         nodeStore,
		logger:            zap.NewNop(),
		HeartbeatInterval: 30,
		HeartbeatWait:     45,
	}

	req := &iop.NodeConfigRefreshRequest{RequestId: "req-1"}
	results := s.PushConfigRefresh(context.Background(), req)

	if len(results) != 1 {
		t.Fatalf("expected 1 result, got %d", len(results))
	}
	if results[0].NodeID != "node-1" {
		t.Fatalf("unexpected node id: %q", results[0].NodeID)
	}
	if results[0].Status != "skipped" {
		t.Fatalf("expected status=skipped for absent registry entry, got %q", results[0].Status)
	}
}

// TestServerPushConfigRefreshEmptyNodeStoreProducesNoResults verifies that
// PushConfigRefresh with an empty NodeStore returns an empty result slice.
func TestServerPushConfigRefreshEmptyNodeStoreProducesNoResults(t *testing.T) {
	nodeStore, err := edgenode.LoadFromConfig(nil)
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}
	s := &Server{
		registry:          edgenode.NewRegistry(),
		nodeStore:         nodeStore,
		logger:            zap.NewNop(),
		HeartbeatInterval: 30,
		HeartbeatWait:     45,
	}
	results := s.PushConfigRefresh(context.Background(), &iop.NodeConfigRefreshRequest{RequestId: "req-empty"})
	if len(results) != 0 {
		t.Fatalf("expected 0 results for empty node store, got %d", len(results))
	}
}

func findCLIAdapter(payload *iop.NodeConfigPayload) *iop.AdapterConfig {
	for _, a := range payload.GetAdapters() {
		if a.GetType() == "cli" {
			return a
		}
	}
	return nil
}

func TestBuildConfigPayload_CLIOneof(t *testing.T) {
	rec := &edgenode.NodeRecord{
		ID:    "node-local",
		Alias: "local",
		Adapters: config.AdaptersConf{
			CLI: config.CLIConf{
				Enabled: true,
				Profiles: map[string]config.CLIProfileConf{
					"codex": {
						Command:               "codex",
						Args:                  []string{"--foo", "--bar"},
						Env:                   []string{"KEY=val"},
						Persistent:            true,
						Terminal:              true,
						ResponseIdleTimeoutMS: 1500,
						StartupIdleTimeoutMS:  300,
						OutputFormat:          "codex-json",
					},
				},
			},
		},
	}

	payload, err := edgenode.BuildConfigPayload(rec)
	if err != nil {
		t.Fatalf("build config payload: %v", err)
	}

	cli := findCLIAdapter(payload)
	if cli == nil {
		t.Fatal("expected cli adapter")
	}
	if cli.GetSettings() != nil {
		t.Fatalf("expected settings to be nil for cli adapter, got %v", cli.GetSettings())
	}
	cliCfg := cli.GetCli()
	if cliCfg == nil {
		t.Fatal("expected typed cli config")
	}
	prof, ok := cliCfg.GetProfiles()["codex"]
	if !ok {
		t.Fatal("expected codex profile")
	}
	if prof.GetCommand() != "codex" {
		t.Fatalf("command: %q", prof.GetCommand())
	}
	if got := prof.GetArgs(); len(got) != 2 || got[0] != "--foo" || got[1] != "--bar" {
		t.Fatalf("args: %#v", got)
	}
	if got := prof.GetEnv(); len(got) != 1 || got[0] != "KEY=val" {
		t.Fatalf("env: %#v", got)
	}
	if !prof.GetPersistent() {
		t.Fatal("persistent")
	}
	if !prof.GetTerminal() {
		t.Fatal("terminal")
	}
	if prof.GetResponseIdleTimeoutMs() != 1500 {
		t.Fatalf("response_idle_timeout_ms: %d", prof.GetResponseIdleTimeoutMs())
	}
	if prof.GetStartupIdleTimeoutMs() != 300 {
		t.Fatalf("startup_idle_timeout_ms: %d", prof.GetStartupIdleTimeoutMs())
	}
	if prof.GetOutputFormat() != "codex-json" {
		t.Fatalf("output_format: %q", prof.GetOutputFormat())
	}
}

func TestBuildConfigPayload_CLICompletionMarker(t *testing.T) {
	rec := &edgenode.NodeRecord{
		ID:    "node-local",
		Alias: "local",
		Adapters: config.AdaptersConf{
			CLI: config.CLIConf{
				Enabled: true,
				Profiles: map[string]config.CLIProfileConf{
					"codex": {
						Command:    "codex",
						Persistent: true,
						CompletionMarker: config.CompletionMarkerConf{
							Line:  "[[DONE]]",
							Regex: `^\[\[DONE\]\]$`,
						},
					},
				},
			},
		},
	}

	payload, err := edgenode.BuildConfigPayload(rec)
	if err != nil {
		t.Fatalf("build config payload: %v", err)
	}
	cli := findCLIAdapter(payload)
	if cli == nil {
		t.Fatal("expected cli adapter")
	}
	prof := cli.GetCli().GetProfiles()["codex"]
	marker := prof.GetCompletionMarker()
	if marker == nil {
		t.Fatal("expected completion_marker")
	}
	if marker.GetLine() != "[[DONE]]" {
		t.Fatalf("line: %q", marker.GetLine())
	}
	if marker.GetRegex() != `^\[\[DONE\]\]$` {
		t.Fatalf("regex: %q", marker.GetRegex())
	}
}

func TestBuildConfigPayload_OllamaVllmOneof(t *testing.T) {
	rec := &edgenode.NodeRecord{
		Adapters: config.AdaptersConf{
			OllamaInstances: []config.OllamaInstanceConf{
				{Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434"},
			},
			VllmInstances: []config.VllmInstanceConf{
				{Name: "vllm", Enabled: true, Endpoint: "http://localhost:8000"},
			},
		},
	}
	payload, err := edgenode.BuildConfigPayload(rec)
	if err != nil {
		t.Fatalf("build: %v", err)
	}
	var ollama, vllm *iop.AdapterConfig
	for _, a := range payload.GetAdapters() {
		switch a.GetType() {
		case "ollama":
			ollama = a
		case "vllm":
			vllm = a
		}
	}
	if ollama == nil || ollama.GetOllama().GetBaseUrl() != "http://localhost:11434" {
		t.Fatalf("ollama: %+v", ollama)
	}
	if vllm == nil || vllm.GetVllm().GetEndpoint() != "http://localhost:8000" {
		t.Fatalf("vllm: %+v", vllm)
	}
}

func TestBuildConfigPayload_AllAdaptersSettingsNil(t *testing.T) {
	rec := &edgenode.NodeRecord{
		Adapters: config.AdaptersConf{
			Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://localhost:11434"},
			Vllm:   config.VllmConf{Enabled: true, Endpoint: "http://localhost:8000"},
			CLI: config.CLIConf{
				Enabled:  true,
				Profiles: map[string]config.CLIProfileConf{"default": {Command: "echo"}},
			},
		},
	}
	payload, err := edgenode.BuildConfigPayload(rec)
	if err != nil {
		t.Fatalf("build: %v", err)
	}
	for _, a := range payload.GetAdapters() {
		if a.GetSettings() != nil {
			t.Fatalf("adapter %q must not populate legacy Settings in new Edge-generated payload", a.GetType())
		}
	}
	var mockFound bool
	for _, a := range payload.GetAdapters() {
		if a.GetType() == "mock" {
			mockFound = true
			if a.GetMock() == nil {
				t.Fatal("mock adapter must use typed MockAdapterConfig oneof")
			}
		}
	}
	if !mockFound {
		t.Fatal("expected mock adapter in payload")
	}
}
