package transport_test

import (
	"context"
	"fmt"
	"net"
	"testing"
	"time"

	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"

	toki "git.toki-labs.com/toki/proto-socket/go"
	"git.toki-labs.com/toki/proto-socket/go/packets"

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

func getFreePort(t *testing.T) string {
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	addr := l.Addr().String()
	l.Close()
	return addr
}

func dialNode(t *testing.T, ctx context.Context, listenAddr string) *toki.TcpClient {
	t.Helper()
	host, portStr, _ := net.SplitHostPort(listenAddr)
	port := 0
	fmt.Sscanf(portStr, "%d", &port)
	nodeParser := toki.ParserMap{
		toki.TypeNameOf(&iop.RegisterResponse{}): func(b []byte) (proto.Message, error) {
			m := &iop.RegisterResponse{}
			return m, proto.Unmarshal(b, m)
		},
	}
	client, err := toki.DialTcp(ctx, host, port, 30, 10, nodeParser)
	if err != nil {
		t.Fatalf("dial edge server: %v", err)
	}
	return client
}

func waitForRegistryEntry(ctx context.Context, registry *edgenode.Registry, nodeID string) (*edgenode.NodeEntry, bool) {
	ticker := time.NewTicker(25 * time.Millisecond)
	defer ticker.Stop()

	for {
		if entry, ok := registry.Get(nodeID); ok {
			return entry, true
		}

		select {
		case <-ctx.Done():
			return nil, false
		case <-ticker.C:
		}
	}
}

func waitForRegistryAbsent(ctx context.Context, registry *edgenode.Registry, nodeID string) bool {
	ticker := time.NewTicker(25 * time.Millisecond)
	defer ticker.Stop()

	for {
		if _, ok := registry.Get(nodeID); !ok {
			return true
		}

		select {
		case <-ctx.Done():
			return false
		case <-ticker.C:
		}
	}
}

func TestEdgeServerGenericRegistrationKind(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	logger := zap.NewNop()
	listenAddr := getFreePort(t)
	registry := edgenode.NewRegistry()
	nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
		{
			ID:        "node-test-01",
			Alias:     "generic-node",
			Token:     "node-token",
			AgentKind: config.AgentKindGenericNode,
		},
	})
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}

	server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
	if err != nil {
		t.Fatalf("new server: %v", err)
	}
	if err := server.Start(ctx); err != nil {
		t.Fatalf("start server: %v", err)
	}
	defer server.Stop()

	client := dialNode(t, ctx, listenAddr)
	defer client.Close()

	resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client.Communicator,
		&iop.RegisterRequest{Token: "node-token"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("register request: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted register response, got reason %q", resp.GetReason())
	}

	entry, ok := waitForRegistryEntry(ctx, registry, "node-test-01")
	if !ok {
		t.Fatal("node was not registered in edge registry within timeout")
	}
	if entry.AgentKind != config.AgentKindGenericNode {
		t.Fatalf("agent kind: got %q want %q", entry.AgentKind, config.AgentKindGenericNode)
	}
	if entry.LifecycleState != edgenode.LifecycleConnected {
		t.Fatalf("lifecycle: got %q want %q", entry.LifecycleState, edgenode.LifecycleConnected)
	}
}

func TestEdgeServerDuplicateRegistrationReason(t *testing.T) {
	cases := []struct {
		name       string
		kind       string
		wantReason string
	}{
		{"generic", config.AgentKindGenericNode, "node already connected"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()

			logger := zap.NewNop()
			listenAddr := getFreePort(t)
			registry := edgenode.NewRegistry()
			nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
				{ID: "dup-01", Alias: "dup", Token: "dup-token", AgentKind: tc.kind},
			})
			if err != nil {
				t.Fatalf("load node store: %v", err)
			}

			server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
			if err != nil {
				t.Fatalf("new server: %v", err)
			}
			if err := server.Start(ctx); err != nil {
				t.Fatalf("start server: %v", err)
			}
			defer server.Stop()

			first := dialNode(t, ctx, listenAddr)
			defer first.Close()
			resp1, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
				&first.Communicator, &iop.RegisterRequest{Token: "dup-token"}, 2*time.Second)
			if err != nil {
				t.Fatalf("first register: %v", err)
			}
			if !resp1.GetAccepted() {
				t.Fatalf("expected first registration accepted, got reason %q", resp1.GetReason())
			}
			if _, ok := waitForRegistryEntry(ctx, registry, "dup-01"); !ok {
				t.Fatal("first registration not visible in registry within timeout")
			}

			second := dialNode(t, ctx, listenAddr)
			defer second.Close()
			resp2, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
				&second.Communicator, &iop.RegisterRequest{Token: "dup-token"}, 2*time.Second)
			if err != nil {
				t.Fatalf("second register: %v", err)
			}
			if resp2.GetAccepted() {
				t.Fatal("expected second registration to be rejected")
			}
			if resp2.GetReason() != tc.wantReason {
				t.Fatalf("duplicate reason: got %q want %q", resp2.GetReason(), tc.wantReason)
			}
		})
	}
}

func TestEdgeServerReconnectAfterUnregisterAccepted(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	logger := zap.NewNop()
	listenAddr := getFreePort(t)
	registry := edgenode.NewRegistry()
	nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
		{ID: "reconnect-01", Alias: "reconnect", Token: "reconnect-token", AgentKind: config.AgentKindGenericNode},
	})
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}

	server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
	if err != nil {
		t.Fatalf("new server: %v", err)
	}
	if err := server.Start(ctx); err != nil {
		t.Fatalf("start server: %v", err)
	}
	defer server.Stop()

	first := dialNode(t, ctx, listenAddr)
	resp1, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&first.Communicator, &iop.RegisterRequest{Token: "reconnect-token"}, 2*time.Second)
	if err != nil {
		t.Fatalf("first register: %v", err)
	}
	if !resp1.GetAccepted() {
		t.Fatalf("expected first registration accepted, got reason %q", resp1.GetReason())
	}
	if _, ok := waitForRegistryEntry(ctx, registry, "reconnect-01"); !ok {
		t.Fatal("first registration not visible in registry within timeout")
	}

	if err := first.Close(); err != nil {
		t.Fatalf("close first client: %v", err)
	}
	if ok := waitForRegistryAbsent(ctx, registry, "reconnect-01"); !ok {
		t.Fatal("registry still contains node after disconnect")
	}

	second := dialNode(t, ctx, listenAddr)
	defer second.Close()
	resp2, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&second.Communicator, &iop.RegisterRequest{Token: "reconnect-token"}, 2*time.Second)
	if err != nil {
		t.Fatalf("second register: %v", err)
	}
	if !resp2.GetAccepted() {
		t.Fatalf("expected reconnect after unregister accepted, got reason %q", resp2.GetReason())
	}
	if _, ok := waitForRegistryEntry(ctx, registry, "reconnect-01"); !ok {
		t.Fatal("reconnect registration not visible in registry within timeout")
	}
}

func TestEdgeServerIntegration(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	logger := zap.NewNop()
	listenAddr := getFreePort(t)
	registry := edgenode.NewRegistry()
	nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
		{
			ID:    "node-test-01",
			Alias: "test-node",
			Token: "test-token",
			Adapters: config.AdaptersConf{
				Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://localhost:11434"},
			},
			Runtime: config.RuntimeConf{Concurrency: 2},
		},
	})
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}

	// 1. Edge 서버 구동
	server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
	if err != nil {
		t.Fatalf("failed to create server: %v", err)
	}
	nodeEventCh := make(chan *iop.EdgeNodeEvent, 4)
	server.SetNodeEventHandler(func(event *iop.EdgeNodeEvent) {
		nodeEventCh <- event
	})
	if err := server.Start(ctx); err != nil {
		t.Fatalf("failed to start server: %v", err)
	}
	defer server.Stop()

	// 2. Mock Node 클라이언트 접속
	host, portStr, _ := net.SplitHostPort(listenAddr)
	port := 0
	fmt.Sscanf(portStr, "%d", &port)

	// Node parser map (mock)
	nodeParser := toki.ParserMap{
		toki.TypeNameOf(&iop.RegisterResponse{}): func(b []byte) (proto.Message, error) {
			m := &iop.RegisterResponse{}
			return m, proto.Unmarshal(b, m)
		},
		toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
			m := &iop.RunRequest{}
			return m, proto.Unmarshal(b, m)
		},
	}

	client, err := toki.DialTcp(ctx, host, port, 30, 10, nodeParser)
	if err != nil {
		t.Fatalf("failed to dial edge server: %v", err)
	}
	defer client.Close()

	// 3. Node가 RegisterRequest를 보내고 RegisterResponse를 받는다
	resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client.Communicator,
		&iop.RegisterRequest{Token: "test-token"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("register request: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted register response, got reason %q", resp.GetReason())
	}
	rec, ok := nodeStore.FindByToken("test-token")
	if !ok {
		t.Fatal("expected record for test-token in store")
	}
	wantNodeID := rec.ID
	if wantNodeID != "node-test-01" {
		t.Fatalf("expected stored id to match explicit config id, got %q", wantNodeID)
	}
	if resp.GetNodeId() != wantNodeID {
		t.Fatalf("expected node id %q, got %q", wantNodeID, resp.GetNodeId())
	}
	if resp.GetAlias() != "test-node" {
		t.Fatalf("expected alias %q, got %q", "test-node", resp.GetAlias())
	}
	if resp.GetConfig().GetRuntime().GetConcurrency() != 2 {
		t.Fatalf("expected runtime concurrency 2, got %d", resp.GetConfig().GetRuntime().GetConcurrency())
	}
	if len(resp.GetConfig().GetAdapters()) != 2 {
		t.Fatalf("expected mock and ollama adapters, got %d", len(resp.GetConfig().GetAdapters()))
	}
	select {
	case event := <-nodeEventCh:
		if event.GetType() != eventpkg.TypeNodeConnected {
			t.Fatalf("event type: got %q want %q", event.GetType(), eventpkg.TypeNodeConnected)
		}
		if event.GetNodeId() != wantNodeID || event.GetAlias() != "test-node" || event.GetReason() != eventpkg.ReasonRegistered {
			t.Fatalf("unexpected connected event: %+v", event)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for node connected event")
	}

	// 4. Registry 등록 여부 확인
	entry, ok := waitForRegistryEntry(ctx, registry, wantNodeID)
	if !ok {
		t.Fatal("node was not registered in edge registry within timeout")
	}

	if got := registry.Count(); got != 1 {
		t.Fatalf("expected registry count 1, got %d", got)
	}
	if entry == nil {
		t.Fatal("expected registry entry, got nil")
	}
	if got := entry.NodeID; got != wantNodeID {
		t.Fatalf("expected node id %q, got %q", wantNodeID, got)
	}
	if got := entry.Alias; got != "test-node" {
		t.Fatalf("expected alias %q, got %q", "test-node", got)
	}

	// 5. Alias로 Resolve 확인
	if e, err := registry.Resolve("test-node"); err != nil || e.NodeID != wantNodeID {
		t.Fatalf("failed to resolve by alias %q: %v", "test-node", err)
	}

	if err := client.Close(); err != nil {
		t.Fatalf("close client: %v", err)
	}
	select {
	case event := <-nodeEventCh:
		if event.GetType() != eventpkg.TypeNodeDisconnected {
			t.Fatalf("event type: got %q want %q", event.GetType(), eventpkg.TypeNodeDisconnected)
		}
		if event.GetNodeId() != wantNodeID || event.GetAlias() != "test-node" || event.GetReason() != eventpkg.ReasonTransportClosed {
			t.Fatalf("unexpected disconnected event: %+v", event)
		}
		if event.GetMetadata()[eventpkg.MetadataTransportCloseReason] != toki.DisconnectReasonRemoteClosed {
			t.Fatalf("transport close reason: got %q want %q", event.GetMetadata()[eventpkg.MetadataTransportCloseReason], toki.DisconnectReasonRemoteClosed)
		}
		if event.GetMetadata()[eventpkg.MetadataTransportCloseError] == "" {
			t.Fatalf("expected transport close error metadata, got %+v", event.GetMetadata())
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for node disconnected event")
	}
}

func TestEdgeServerRegistrationFailureReasons(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	logger := zap.NewNop()
	listenAddr := getFreePort(t)
	registry := edgenode.NewRegistry()
	nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
		{
			ID:        "node-fail-01",
			Alias:     "fail-node",
			Token:     "valid-token",
			AgentKind: config.AgentKindGenericNode,
		},
	})
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}

	server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
	if err != nil {
		t.Fatalf("new server: %v", err)
	}

	nodeEventCh := make(chan *iop.EdgeNodeEvent, 10)
	server.SetNodeEventHandler(func(event *iop.EdgeNodeEvent) {
		nodeEventCh <- event
	})

	if err := server.Start(ctx); err != nil {
		t.Fatalf("start server: %v", err)
	}
	defer server.Stop()

	// 1. Unknown Token Test
	client1 := dialNode(t, ctx, listenAddr)
	resp1, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client1.Communicator,
		&iop.RegisterRequest{Token: "unknown-token-12345"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("send register 1: %v", err)
	}
	if resp1.GetAccepted() {
		t.Fatal("expected rejected")
	}

	select {
	case event := <-nodeEventCh:
		if event.GetType() != eventpkg.TypeNodeRegistrationFailed {
			t.Fatalf("expected registration failed type, got %q", event.GetType())
		}
		if event.GetReason() != eventpkg.ReasonUnknownToken {
			t.Fatalf("expected reason unknown_token, got %q", event.GetReason())
		}
		if event.GetMetadata()[eventpkg.MetadataFailureReason] != eventpkg.ReasonUnknownToken {
			t.Fatalf("expected metadata failure reason, got %q", event.GetMetadata()[eventpkg.MetadataFailureReason])
		}
		if event.GetMetadata()[eventpkg.MetadataTokenPrefix] != "unknown-..." {
			t.Fatalf("expected token prefix unknown-..., got %q", event.GetMetadata()[eventpkg.MetadataTokenPrefix])
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for unknown token registration failed event")
	}
	client1.Close()

	// 2. Duplicate Connection Test
	client2 := dialNode(t, ctx, listenAddr)
	resp2, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client2.Communicator,
		&iop.RegisterRequest{Token: "valid-token"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("send register 2: %v", err)
	}
	if !resp2.GetAccepted() {
		t.Fatalf("expected register 2 accepted, got %q", resp2.GetReason())
	}
	// Consume connection event
	<-nodeEventCh

	client3 := dialNode(t, ctx, listenAddr)
	resp3, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client3.Communicator,
		&iop.RegisterRequest{Token: "valid-token"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("send register 3: %v", err)
	}
	if resp3.GetAccepted() {
		t.Fatal("expected duplicate connection registration rejected")
	}

	select {
	case event := <-nodeEventCh:
		if event.GetType() != eventpkg.TypeNodeRegistrationFailed {
			t.Fatalf("expected registration failed type, got %q", event.GetType())
		}
		if event.GetReason() != eventpkg.ReasonDuplicateConnection {
			t.Fatalf("expected reason duplicate_connection, got %q", event.GetReason())
		}
		if event.GetMetadata()[eventpkg.MetadataFailureReason] != eventpkg.ReasonDuplicateConnection {
			t.Fatalf("expected failure reason metadata, got %q", event.GetMetadata()[eventpkg.MetadataFailureReason])
		}
		if event.GetMetadata()[eventpkg.MetadataAgentKind] != config.AgentKindGenericNode {
			t.Fatalf("expected agent kind metadata, got %q", event.GetMetadata()[eventpkg.MetadataAgentKind])
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for duplicate registration failed event")
	}
	client2.Close()
	client3.Close()
}

func TestEdgeServerHeartbeatTimeoutDisconnect(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	logger := zap.NewNop()
	listenAddr := getFreePort(t)
	registry := edgenode.NewRegistry()
	nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
		{
			ID:        "node-timeout-01",
			Alias:     "timeout-node",
			Token:     "timeout-token",
			AgentKind: config.AgentKindGenericNode,
		},
	})
	if err != nil {
		t.Fatalf("load node store: %v", err)
	}

	server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
	if err != nil {
		t.Fatalf("new server: %v", err)
	}
	server.HeartbeatInterval = 1
	server.HeartbeatWait = 1

	nodeEventCh := make(chan *iop.EdgeNodeEvent, 10)
	server.SetNodeEventHandler(func(event *iop.EdgeNodeEvent) {
		nodeEventCh <- event
	})

	if err := server.Start(ctx); err != nil {
		t.Fatalf("start server: %v", err)
	}
	defer server.Stop()

	client := dialNode(t, ctx, listenAddr)
	defer client.Close()
	client.RemoveListeners(toki.TypeNameOf(&packets.HeartBeat{}))

	resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client.Communicator,
		&iop.RegisterRequest{Token: "timeout-token"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("register request: %v", err)
	}
	if !resp.GetAccepted() {
		t.Fatalf("expected accepted, got %q", resp.GetReason())
	}

	select {
	case event := <-nodeEventCh:
		if event.GetType() != eventpkg.TypeNodeConnected {
			t.Fatalf("expected connected event, got %q", event.GetType())
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for connected event")
	}

	select {
	case event := <-nodeEventCh:
		if event.GetType() != eventpkg.TypeNodeDisconnected {
			t.Fatalf("expected disconnected event, got %q", event.GetType())
		}
		if event.GetReason() != eventpkg.ReasonHeartbeatTimeout {
			t.Fatalf("expected reason heartbeat_timeout, got %q", event.GetReason())
		}
		if event.GetMetadata()[eventpkg.MetadataFailureReason] != eventpkg.ReasonHeartbeatTimeout {
			t.Fatalf("expected failure reason heartbeat_timeout, got %q", event.GetMetadata()[eventpkg.MetadataFailureReason])
		}
		if event.GetMetadata()[eventpkg.MetadataLifecycleState] != edgenode.LifecycleFailed {
			t.Fatalf("expected lifecycle state failed, got %q", event.GetMetadata()[eventpkg.MetadataLifecycleState])
		}
	case <-time.After(3 * time.Second):
		t.Fatal("timeout waiting for heartbeat timeout disconnected event")
	}
}
