//go:build !windows

package terminal

import (
	"context"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/tmux"
	"github.com/aoagents/agent-orchestrator/backend/internal/domain"
	"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

// TestAttachmentStreamsRealTmuxPane attaches a real PTY to a real tmux session
// and asserts output streams back, then that killing the session stops the
// attachment without a re-attach storm. Skipped when tmux is unavailable.
func TestAttachmentStreamsRealTmuxPane(t *testing.T) {
	if _, err := exec.LookPath("tmux"); err != nil {
		t.Skip("tmux unavailable")
	}
	// See TestAttachmentReattachAdoptsNewSize: tmux needs a usable TERM to attach.
	t.Setenv("TERM", "xterm-256color")

	name := "ao-term-it-" + strconv.Itoa(os.Getpid())
	rt := tmux.New(tmux.Options{Timeout: 10 * time.Second})
	handle, err := rt.Create(context.Background(), ports.RuntimeConfig{
		SessionID:     domain.SessionID(name),
		WorkspacePath: t.TempDir(),
		Argv:          []string{"sh", "-lc", "printf AO_READY\\n; exec sh -i"},
	})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	t.Cleanup(func() { _ = rt.Destroy(context.Background(), handle) })

	var got safeBytes
	a := newAttachment(name, handle, rt, nil, got.add, nil, testLogger())
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go a.run(ctx)

	// Type a unique marker and expect it echoed back through the PTY.
	eventually(t, 3*time.Second, func() bool { return a.write([]byte("echo AO_MARKER_42\n")) == nil })
	eventually(t, 5*time.Second, func() bool { return strings.Contains(got.string(), "AO_MARKER_42") })

	// Kill the session: the attachment must observe it as gone and not re-attach.
	if err := rt.Destroy(context.Background(), handle); err != nil {
		t.Fatalf("Destroy: %v", err)
	}
	eventually(t, 5*time.Second, func() bool { return a.isExited() })
}

// TestAttachmentReattachAdoptsNewSize is the end-to-end regression for the
// stale-size desync: client A holds the session at one grid, detaches, and
// client B immediately attaches at a different grid (the frontend's
// remount/reconnect flow). B's tmux client must adopt B's size, not A's.
func TestAttachmentReattachAdoptsNewSize(t *testing.T) {
	if _, err := exec.LookPath("tmux"); err != nil {
		t.Skip("tmux unavailable")
	}
	// tmux refuses to attach a client without a usable TERM, printing
	// "open terminal failed: terminal does not support clear". The daemon sets a
	// default TERM in production (Finder-launched attach fix); CI runners have
	// none, so set it here to match the real environment.
	t.Setenv("TERM", "xterm-256color")

	name := "ao-term-size-it-" + strconv.Itoa(os.Getpid())
	rt := tmux.New(tmux.Options{Timeout: 10 * time.Second})
	handle, err := rt.Create(context.Background(), ports.RuntimeConfig{
		SessionID:     domain.SessionID(name),
		WorkspacePath: t.TempDir(),
		Argv:          []string{"sh", "-lc", "printf AO_READY\\n; exec sh -i"},
	})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	t.Cleanup(func() { _ = rt.Destroy(context.Background(), handle) })

	attachAt := func(rows, cols uint16) (*attachment, *safeBytes, <-chan struct{}, context.CancelFunc) {
		var got safeBytes
		opened := make(chan struct{})
		a := newAttachment(name, handle, rt, func() { close(opened) }, got.add, nil, testLogger())
		if err := a.resize(rows, cols); err != nil {
			t.Fatalf("record size: %v", err)
		}
		ctx, cancel := context.WithCancel(context.Background())
		go a.run(ctx)
		return a, &got, opened, cancel
	}

	// Client A at 115x37: wait for the pane shell, then detach.
	a, _, openedA, cancelA := attachAt(37, 115)
	select {
	case <-openedA:
	case <-time.After(10 * time.Second):
		t.Fatal("client A did not attach")
	}
	a.close()
	cancelA()

	// Client B re-attaches immediately at 148x40. The inner pane must see B's
	// grid (tmux may shave a row/col; assert cols land near 148 and far from 115).
	b, gotB, openedB, cancelB := attachAt(40, 148)
	defer cancelB()
	defer b.close()
	select {
	case <-openedB:
	case <-time.After(10 * time.Second):
		t.Fatal("client B did not attach")
	}

	// Drive the reattached shell until it reports its width. We RESEND the probe
	// each iteration: onOpen means the stream accepts input, not that the inner
	// `sh -i` is already at a prompt reading stdin after the reattach, so an early
	// keystroke can be dropped; retrying covers that. Real tmux + shell output is
	// also slow under -race on CI, hence the long deadline. On timeout we dump
	// exactly what the pane produced so the failure is self-explaining (e.g. the
	// probe echoed but never executed, or stty errored).
	var captured string
	gotWidth := false
	deadline := time.Now().Add(30 * time.Second)
	for time.Now().Before(deadline) {
		_ = b.write([]byte("echo SIZE:$(stty size)\n"))
		time.Sleep(250 * time.Millisecond)
		captured = gotB.string()
		i := strings.LastIndex(captured, "SIZE:")
		if i < 0 {
			continue
		}
		fields := strings.Fields(strings.TrimPrefix(captured[i:], "SIZE:"))
		if len(fields) < 2 {
			continue
		}
		cols, err := strconv.Atoi(strings.TrimFunc(fields[1], func(r rune) bool { return r < '0' || r > '9' }))
		if err != nil {
			continue
		}
		if cols > 130 { // B's 148 minus any tmux chrome; a stale A-layout reports <=115
			gotWidth = true
			break
		}
	}
	if !gotWidth {
		t.Fatalf("reattached pane never reported B's width (cols>130) within 30s; captured:\n%q", captured)
	}
}
