package bootstrap

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	goruntime "runtime"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

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

	edgeservice "iop/apps/edge/internal/service"
	iop "iop/proto/gen/iop"
)

// TestActualNodeReconnectReadyPumpsQueuedWaiterExactlyOnce covers the hole that
// fake queue and bootstrap tests cannot see independently. It starts the actual
// iop-node entrypoint once, closes it to make the configured resource offline,
// queues a waiter behind an alternate provider's occupied slot, then starts the
// same node entrypoint again. The queued run must reach the real Node handler and
// return exactly one terminal event after the second process completes the
// RegisterResponse → SetHandler → NodeReadyRequest/ack sequence.
func TestActualNodeReconnectReadyPumpsQueuedWaiterExactlyOnce(t *testing.T) {
	if goruntime.GOOS == "windows" {
		t.Skip("the local actual-node fixture uses the POSIX sh CLI profile")
	}
	if _, err := exec.LookPath("sh"); err != nil {
		t.Skipf("sh is required by the local actual-node fixture: %v", err)
	}

	dir := t.TempDir()
	edgeAddr := freeTCPAddr(t)
	edgeConfigPath := writeReconnectReadinessEdgeConfig(t, dir, edgeAddr)
	rt, err := NewRuntime(loadServeNormalizedConfig(t, edgeConfigPath))
	if err != nil {
		t.Fatalf("NewRuntime: %v", err)
	}
	if err := rt.Start(context.Background()); err != nil {
		t.Fatalf("start edge runtime: %v", err)
	}
	defer func() {
		if err := rt.Stop(); err != nil {
			t.Errorf("stop edge runtime: %v", err)
		}
	}()

	alt := dialReconnectReadinessFakeNode(t, edgeAddr)
	defer alt.Close()
	altRuns := make(chan *iop.RunRequest, 4)
	toki.AddListenerTyped[*iop.RunRequest](&alt.Communicator, func(req *iop.RunRequest) {
		altRuns <- req
	})
	altRegistration, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&alt.Communicator, &iop.RegisterRequest{Token: "alt-token"}, fakeNodeHandshakeTimeout)
	if err != nil {
		t.Fatalf("register alternate fake node: %v", err)
	}
	if !altRegistration.GetAccepted() {
		t.Fatalf("alternate registration rejected: %s", altRegistration.GetReason())
	}
	readyFakeNode(t, alt, altRegistration.GetNodeId())
	waitForReconnectReadiness(t, 3*time.Second, "alternate node dispatch-ready", func() bool {
		_, ok := rt.Registry.GetReady("node-alt")
		return ok
	})

	nodeBinary := buildReconnectReadinessNodeBinary(t, dir)
	nodeConfigPath := writeReconnectReadinessNodeConfig(t, dir, edgeAddr)

	// First process establishes the resource, then exits cleanly. The waiter below
	// is therefore testing an actual reconnect, rather than only the first Node
	// registration path.
	first := startReconnectReadinessNode(t, nodeBinary, nodeConfigPath, filepath.Join(dir, "node-first"))
	waitForReconnectReadiness(t, 6*time.Second, "initial actual node ready", func() bool {
		_, ok := rt.Registry.GetReady("node-recon")
		return ok
	})
	if err := first.Stop(); err != nil {
		t.Fatalf("stop initial actual node: %v", err)
	}
	waitForReconnectReadiness(t, 4*time.Second, "initial actual node removed", func() bool {
		_, ok := rt.Registry.Get("node-recon")
		return !ok
	})

	// The alternate provider receives one background run and deliberately keeps
	// its lease until the test sends its terminal event. With the reconnect node
	// offline, the next run must become a provider-pool waiter.
	blocking, err := rt.Service.SubmitRun(context.Background(), edgeservice.SubmitRunRequest{
		ModelGroupKey: "reconnect-ready-model",
		ProviderPool:  true,
		Background:    true,
		Input:         map[string]any{"prompt": "hold alternate capacity"},
	})
	if err != nil {
		t.Fatalf("submit alternate blocking run: %v", err)
	}
	defer blocking.Close()
	select {
	case req := <-altRuns:
		if req.GetRunId() != blocking.Dispatch().RunID {
			t.Fatalf("alternate run id=%q, want %q", req.GetRunId(), blocking.Dispatch().RunID)
		}
	case <-time.After(3 * time.Second):
		t.Fatal("alternate provider did not receive the blocking RunRequest")
	}

	type submitOutcome struct {
		result edgeservice.RunResult
		err    error
	}
	waiterDone := make(chan submitOutcome, 1)
	waiterCtx, cancelWaiter := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancelWaiter()
	go func() {
		result, submitErr := rt.Service.SubmitRun(waiterCtx, edgeservice.SubmitRunRequest{
			ModelGroupKey: "reconnect-ready-model",
			ProviderPool:  true,
			Input:         map[string]any{"prompt": "reconnect-ready waiter"},
		})
		waiterDone <- submitOutcome{result: result, err: submitErr}
	}()

	waitForProviderSnapshot(t, rt.Service, "node-alt", "provider-alt", 1, 1, 0, 0)
	select {
	case outcome := <-waiterDone:
		if outcome.result != nil {
			outcome.result.Close()
		}
		t.Fatalf("waiter resolved before reconnect: %v", outcome.err)
	default:
	}

	second := startReconnectReadinessNode(t, nodeBinary, nodeConfigPath, filepath.Join(dir, "node-second"))
	waitForReconnectReadiness(t, 6*time.Second, "reconnected actual node ready", func() bool {
		_, ok := rt.Registry.GetReady("node-recon")
		return ok
	})

	var waiter submitOutcome
	select {
	case waiter = <-waiterDone:
	case <-time.After(6 * time.Second):
		t.Fatal("queued waiter was not dispatched after actual node ready ack")
	}
	if waiter.err != nil {
		t.Fatalf("queued waiter failed after reconnect: %v", waiter.err)
	}
	if waiter.result == nil {
		t.Fatal("queued waiter returned nil result")
	}
	defer waiter.result.Close()
	dispatch := waiter.result.Dispatch()
	if dispatch.NodeID != "node-recon" || dispatch.ProviderID != "provider-recon" {
		t.Fatalf("waiter dispatch=(node=%q provider=%q), want node-recon/provider-recon", dispatch.NodeID, dispatch.ProviderID)
	}
	if dispatch.Adapter != "cli" || dispatch.Target != "recon-target" {
		t.Fatalf("waiter dispatch=(adapter=%q target=%q), want cli/recon-target", dispatch.Adapter, dispatch.Target)
	}
	if dispatch.QueueReason != "capacity_full" {
		t.Fatalf("waiter queue reason=%q, want capacity_full", dispatch.QueueReason)
	}
	waitForProviderSnapshot(t, rt.Service, "node-recon", "provider-recon", 1, 0, 0, 0)

	// A rejected duplicate connection cannot turn itself ready or pump another
	// waiter while the actual reconnect owner executes its RunRequest.
	duplicate := dialReconnectReadinessFakeNode(t, edgeAddr)
	defer duplicate.Close()
	dupRegistration, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&duplicate.Communicator, &iop.RegisterRequest{Token: "recon-token"}, fakeNodeHandshakeTimeout)
	if err != nil {
		t.Fatalf("duplicate register request: %v", err)
	}
	if dupRegistration.GetAccepted() {
		t.Fatal("duplicate reconnect registration was accepted")
	}
	dupReady, err := toki.SendRequestTyped[*iop.NodeReadyRequest, *iop.NodeReadyResponse](
		&duplicate.Communicator, &iop.NodeReadyRequest{NodeId: "node-recon"}, fakeNodeHandshakeTimeout)
	if err != nil {
		t.Fatalf("duplicate ready request: %v", err)
	}
	if dupReady.GetReady() {
		t.Fatal("duplicate connection received a successful ready acknowledgement")
	}

	terminalCount := waitForRunTerminal(t, waiter.result.Stream().Events, dispatch.RunID)
	if terminalCount != 1 {
		t.Fatalf("actual node terminal count=%d, want exactly 1", terminalCount)
	}
	waitForReconnectReadiness(t, 3*time.Second, "actual node payload", func() bool {
		return strings.Contains(second.Output(), "reconnect-ready-terminal")
	})

	// Late terminal delivery is idempotent and must neither re-release the lease
	// nor dispatch another request. The primary terminal above came from the
	// actual node process; this is only the competing callback regression probe.
	rt.Service.HandleRunLifecycleEvent(&iop.RunEvent{RunId: dispatch.RunID, Type: "complete", NodeId: "node-recon"})
	waitForProviderSnapshot(t, rt.Service, "node-recon", "provider-recon", 0, 0, 0, 0)
	select {
	case unexpected := <-altRuns:
		t.Fatalf("unexpected extra alternate dispatch after duplicate ready/terminal: %q", unexpected.GetRunId())
	default:
	}

	if err := alt.Send(&iop.RunEvent{RunId: blocking.Dispatch().RunID, Type: "complete", NodeId: "node-alt"}); err != nil {
		t.Fatalf("send alternate terminal: %v", err)
	}
	waitForProviderSnapshot(t, rt.Service, "node-alt", "provider-alt", 0, 0, 0, 0)
	assertReconnectReadinessCountersZero(t, rt.Service)

	if err := second.Stop(); err != nil {
		t.Fatalf("stop reconnected actual node: %v", err)
	}
}

func reconnectReadinessParserMap() toki.ParserMap {
	return toki.ParserMap{
		toki.TypeNameOf(&iop.RegisterResponse{}): func(b []byte) (proto.Message, error) {
			m := &iop.RegisterResponse{}
			return m, proto.Unmarshal(b, m)
		},
		toki.TypeNameOf(&iop.NodeReadyResponse{}): func(b []byte) (proto.Message, error) {
			m := &iop.NodeReadyResponse{}
			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)
		},
	}
}

func dialReconnectReadinessFakeNode(t *testing.T, edgeAddr string) *toki.TcpClient {
	t.Helper()
	host, portText, err := net.SplitHostPort(edgeAddr)
	if err != nil {
		t.Fatalf("split edge address %q: %v", edgeAddr, err)
	}
	port, err := strconv.Atoi(portText)
	if err != nil {
		t.Fatalf("parse edge port %q: %v", portText, err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	client, err := toki.DialTcp(ctx, host, port, 30, 10, reconnectReadinessParserMap())
	if err != nil {
		t.Fatalf("dial edge: %v", err)
	}
	return client
}

func writeReconnectReadinessEdgeConfig(t *testing.T, dir, edgeAddr string) string {
	t.Helper()
	path := filepath.Join(dir, "edge.yaml")
	yaml := fmt.Sprintf(`
server:
  listen: %q
bootstrap:
  listen: "127.0.0.1:0"
  artifact_dir: %q
logging:
  level: "error"
refresh:
  enabled: false
  listen: "127.0.0.1:0"
openai:
  enabled: false
  listen: "127.0.0.1:0"
a2a:
  enabled: false
  listen: "127.0.0.1:0"
metrics:
  port: 0
provider_pool:
  max_queue: 4
  queue_timeout_ms: 8000
models:
  - id: "reconnect-ready-model"
    providers:
      provider-alt: "alt-target"
      provider-recon: "recon-target"
nodes:
  - id: "node-alt"
    alias: "alternate"
    token: "alt-token"
    adapters:
      cli:
        enabled: true
        profiles:
          alt-target:
            command: "sh"
            args: ["-c", "sleep 30"]
    providers:
      - id: "provider-alt"
        type: "cli"
        category: "cli"
        adapter: "cli"
        models: ["alt-target"]
        health: "available"
        capacity: 1
  - id: "node-recon"
    alias: "reconnect"
    token: "recon-token"
    adapters:
      cli:
        enabled: true
        profiles:
          recon-target:
            command: "sh"
            args: ["-c", "sleep 0.4; printf 'reconnect-ready-terminal\\n'"]
    providers:
      - id: "provider-recon"
        type: "cli"
        category: "cli"
        adapter: "cli"
        models: ["recon-target"]
        health: "available"
        capacity: 1
`, edgeAddr, filepath.Join(dir, "artifacts"))
	if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
		t.Fatalf("write edge config: %v", err)
	}
	return path
}

func writeReconnectReadinessNodeConfig(t *testing.T, dir, edgeAddr string) string {
	t.Helper()
	path := filepath.Join(dir, "node.yaml")
	yaml := fmt.Sprintf(`
transport:
  edge_addr: %q
  token: "recon-token"
reconnect:
  interval_sec: 1
  max_attempts: 1
logging:
  level: "error"
  path: %q
metrics:
  port: 0
`, edgeAddr, filepath.Join(dir, "node.log"))
	if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
		t.Fatalf("write node config: %v", err)
	}
	return path
}

func buildReconnectReadinessNodeBinary(t *testing.T, dir string) string {
	t.Helper()
	root := reconnectReadinessRepoRoot(t)
	binary := filepath.Join(dir, "iop-node")
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "go", "build", "-o", binary, "./apps/node/cmd/node")
	cmd.Dir = root
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("build actual iop-node: %v\n%s", err, output)
	}
	return binary
}

func reconnectReadinessRepoRoot(t *testing.T) string {
	t.Helper()
	dir, err := os.Getwd()
	if err != nil {
		t.Fatalf("getwd: %v", err)
	}
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			return dir
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			t.Fatal("could not locate repository go.mod")
		}
		dir = parent
	}
}

type reconnectReadinessNodeProcess struct {
	cmd        *exec.Cmd
	stdoutPath string
	stderrPath string
	stdout     *os.File
	stderr     *os.File
	wait       chan error
	once       sync.Once
	err        error
}

func startReconnectReadinessNode(t *testing.T, binary, configPath, dir string) *reconnectReadinessNodeProcess {
	t.Helper()
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatalf("create node work directory: %v", err)
	}
	stdoutPath := filepath.Join(dir, "stdout.log")
	stderrPath := filepath.Join(dir, "stderr.log")
	stdout, err := os.Create(stdoutPath)
	if err != nil {
		t.Fatalf("create node stdout log: %v", err)
	}
	stderr, err := os.Create(stderrPath)
	if err != nil {
		_ = stdout.Close()
		t.Fatalf("create node stderr log: %v", err)
	}

	cmd := exec.Command(binary, "--config", configPath, "serve")
	cmd.Dir = dir
	cmd.Stdout = stdout
	cmd.Stderr = stderr
	if err := cmd.Start(); err != nil {
		_ = stdout.Close()
		_ = stderr.Close()
		t.Fatalf("start actual iop-node: %v", err)
	}
	p := &reconnectReadinessNodeProcess{
		cmd:        cmd,
		stdoutPath: stdoutPath,
		stderrPath: stderrPath,
		stdout:     stdout,
		stderr:     stderr,
		wait:       make(chan error, 1),
	}
	go func() { p.wait <- cmd.Wait() }()
	t.Cleanup(func() {
		if err := p.Stop(); err != nil && !t.Failed() {
			t.Errorf("stop actual iop-node during cleanup: %v", err)
		}
		if t.Failed() {
			t.Logf("actual iop-node stdout (%s):\n%s", p.stdoutPath, p.Output())
			t.Logf("actual iop-node stderr (%s):\n%s", p.stderrPath, p.ErrorOutput())
		}
	})
	return p
}

func (p *reconnectReadinessNodeProcess) Stop() error {
	p.once.Do(func() {
		if p.cmd.ProcessState == nil {
			if err := p.cmd.Process.Signal(os.Interrupt); err != nil && !strings.Contains(err.Error(), "process already finished") {
				p.err = err
			}
		}
		select {
		case waitErr := <-p.wait:
			if p.err == nil {
				p.err = waitErr
			}
		case <-time.After(5 * time.Second):
			_ = p.cmd.Process.Kill()
			waitErr := <-p.wait
			if p.err == nil {
				p.err = fmt.Errorf("iop-node did not stop after interrupt: %w", waitErr)
			}
		}
		_ = p.stdout.Close()
		_ = p.stderr.Close()
	})
	return p.err
}

func (p *reconnectReadinessNodeProcess) Output() string {
	b, err := os.ReadFile(p.stdoutPath)
	if err != nil {
		return fmt.Sprintf("read stdout: %v", err)
	}
	return string(b)
}

func (p *reconnectReadinessNodeProcess) ErrorOutput() string {
	b, err := os.ReadFile(p.stderrPath)
	if err != nil {
		return fmt.Sprintf("read stderr: %v", err)
	}
	return string(b)
}

func waitForReconnectReadiness(t *testing.T, timeout time.Duration, description string, condition func() bool) {
	t.Helper()
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if condition() {
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	if condition() {
		return
	}
	t.Fatalf("timed out waiting for %s", description)
}

func waitForProviderSnapshot(t *testing.T, service *edgeservice.Service, nodeID, providerID string, inFlight, queued, longInFlight, longQueued int32) {
	t.Helper()
	waitForReconnectReadiness(t, 4*time.Second, fmt.Sprintf("provider snapshot %s/%s", nodeID, providerID), func() bool {
		for _, node := range service.ListNodeSnapshots() {
			if node.NodeID != nodeID {
				continue
			}
			for _, provider := range node.ProviderSnapshots {
				if provider.GetId() == providerID {
					return provider.GetInFlight() == inFlight && provider.GetQueued() == queued &&
						provider.GetLongInFlight() == longInFlight && provider.GetLongQueued() == longQueued
				}
			}
		}
		return false
	})
}

func waitForRunTerminal(t *testing.T, events <-chan *iop.RunEvent, runID string) int {
	t.Helper()
	terminalCount := 0
	deadline := time.NewTimer(6 * time.Second)
	defer deadline.Stop()
	for terminalCount == 0 {
		select {
		case event := <-events:
			if event != nil && event.GetRunId() == runID && isReconnectTerminal(event.GetType()) {
				terminalCount++
			}
		case <-deadline.C:
			t.Fatalf("timed out waiting for actual node terminal run_id=%q", runID)
		}
	}

	quiet := time.NewTimer(300 * time.Millisecond)
	defer quiet.Stop()
	for {
		select {
		case event := <-events:
			if event != nil && event.GetRunId() == runID && isReconnectTerminal(event.GetType()) {
				terminalCount++
			}
		case <-quiet.C:
			return terminalCount
		}
	}
}

func isReconnectTerminal(eventType string) bool {
	switch eventType {
	case "complete", "error", "cancelled":
		return true
	default:
		return false
	}
}

func assertReconnectReadinessCountersZero(t *testing.T, service *edgeservice.Service) {
	t.Helper()
	for _, node := range service.ListNodeSnapshots() {
		for _, provider := range node.ProviderSnapshots {
			if provider.GetInFlight() != 0 || provider.GetQueued() != 0 || provider.GetLongInFlight() != 0 || provider.GetLongQueued() != 0 {
				t.Errorf("provider counters not settled node=%q provider=%q normal=%d queued=%d long=%d long_queued=%d",
					node.NodeID, provider.GetId(), provider.GetInFlight(), provider.GetQueued(), provider.GetLongInFlight(), provider.GetLongQueued())
			}
		}
	}
}
