package bootstrap

import (
	"context"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"iop/packages/go/config"
)

func TestNewRuntimeSeedsNodeStore(t *testing.T) {
	cfg := newTestConfig()
	rt, err := NewRuntime(cfg)
	if err != nil {
		t.Fatalf("NewRuntime: %v", err)
	}
	if rt.NodeStore == nil {
		t.Fatal("NodeStore is nil")
	}
	rec, ok := rt.NodeStore.FindByToken("tok-1")
	if !ok {
		t.Fatal("expected node record seeded by token tok-1")
	}
	if rec.ID != "node-1" || rec.Alias != "alias-1" {
		t.Errorf("unexpected record: id=%q alias=%q", rec.ID, rec.Alias)
	}
	if rt.Registry == nil || rt.EventBus == nil || rt.Service == nil || rt.Server == nil || rt.Logger == nil {
		t.Error("runtime dependencies must be wired by NewRuntime")
	}
}

func TestRuntimeWiresTransportHandlers(t *testing.T) {
	cfg := newTestConfig()
	rt, err := NewRuntime(cfg)
	if err != nil {
		t.Fatalf("NewRuntime: %v", err)
	}
	if rt.Server.HasRunEventHandler() || rt.Server.HasNodeEventHandler() {
		t.Fatal("handlers must not be wired before wireHandlers")
	}
	if rt.Server.HasRunLifecycleHandler() || rt.Server.HasNodeDisconnectHandler() {
		t.Fatal("lifecycle handlers must not be wired before wireHandlers")
	}
	rt.wireHandlers()
	if !rt.Server.HasRunEventHandler() {
		t.Error("run event handler not wired")
	}
	if !rt.Server.HasNodeEventHandler() {
		t.Error("node event handler not wired")
	}
	// Without these the event bus would be the only path back to the service,
	// and a dropped fanout would strand provider leases.
	if !rt.Server.HasRunLifecycleHandler() {
		t.Error("run lifecycle handler not wired")
	}
	if !rt.Server.HasNodeDisconnectHandler() {
		t.Error("node disconnect handler not wired")
	}
}

func TestNewRuntimeWiresInputManager(t *testing.T) {
	cfg := newTestConfig()
	rt, err := NewRuntime(cfg)
	if err != nil {
		t.Fatalf("NewRuntime: %v", err)
	}
	if rt.Input == nil {
		t.Fatal("Input manager is nil")
	}
	if rt.Input.OpenAI == nil {
		t.Fatal("Input.OpenAI is nil")
	}
	if rt.Input.A2A == nil {
		t.Fatal("Input.A2A is nil")
	}
}

func TestRuntimeKeepsListenersAfterStartupContextCancel(t *testing.T) {
	edgeAddr := freeTCPAddr(t)
	openAIAddr := freeTCPAddr(t)
	cfg := newTestConfig()
	cfg.Server.Listen = edgeAddr
	cfg.OpenAI = config.EdgeOpenAIConf{
		Enabled: true,
		Listen:  openAIAddr,
	}

	rt, err := NewRuntime(cfg)
	if err != nil {
		t.Fatalf("NewRuntime: %v", err)
	}
	startupCtx, cancelStartup := context.WithCancel(context.Background())
	if err := rt.Start(startupCtx); err != nil {
		t.Fatalf("Start: %v", err)
	}
	defer func() {
		if err := rt.Stop(); err != nil {
			t.Fatalf("Stop: %v", err)
		}
	}()

	cancelStartup()

	conn, err := net.DialTimeout("tcp", edgeAddr, time.Second)
	if err != nil {
		t.Fatalf("edge listener closed with startup context: %v", err)
	}
	_ = conn.Close()

	client := &http.Client{Timeout: time.Second}
	resp, err := client.Get("http://" + openAIAddr + "/healthz")
	if err != nil {
		t.Fatalf("openai listener closed with startup context: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("openai health status = %d, want %d", resp.StatusCode, http.StatusOK)
	}
}

func TestNewRuntimeWritesLogsToConfiguredPath(t *testing.T) {
	dir := t.TempDir()
	logPath := filepath.Join(dir, "logs", "edge.log")

	cfg := newTestConfig()
	cfg.Logging.Level = "info"
	cfg.Logging.Path = logPath
	rt, err := NewRuntime(cfg)
	if err != nil {
		t.Fatalf("NewRuntime: %v", err)
	}
	rt.Logger.Info("runtime-log-entry")
	_ = rt.Logger.Sync()

	info, err := os.Stat(logPath)
	if err != nil {
		t.Fatalf("stat log path: %v", err)
	}
	if info.Size() == 0 {
		t.Fatalf("log file empty: %s", logPath)
	}
	contents, err := os.ReadFile(logPath)
	if err != nil {
		t.Fatalf("read log file: %v", err)
	}
	if !strings.Contains(string(contents), "runtime-log-entry") {
		t.Fatalf("expected runtime-log-entry in log file; contents=%q", string(contents))
	}
}

func TestNewRuntimeRejectsDuplicateToken(t *testing.T) {
	cfg := &config.EdgeConfig{
		Server:  config.EdgeServerConf{Listen: "127.0.0.1:0"},
		Logging: config.LoggingConf{Level: "error"},
		Nodes: []config.NodeDefinition{
			{ID: "node-1", Alias: "a1", Token: "dup"},
			{ID: "node-2", Alias: "a2", Token: "dup"},
		},
	}
	if _, err := NewRuntime(cfg); err == nil {
		t.Fatal("expected error on duplicate token seed")
	}
}
