//go:build !windows

package terminal

import (
	"context"
	"strings"
	"testing"
	"time"
)

// TestCreackPTYCloseIsIdempotent guards the shutdown deadlock: the session run
// loop and session.close both call Close on the same PTY, so cmd.Wait must run
// exactly once. Without the sync.Once a second Wait blocks forever, so this test
// would hang (caught by the watchdog) rather than fail.
func TestCreackPTYCloseIsIdempotent(t *testing.T) {
	p, err := defaultSpawn(context.Background(), []string{"/bin/sh", "-c", "sleep 30"}, 0, 0)
	if err != nil {
		t.Fatalf("spawn: %v", err)
	}

	done := make(chan struct{})
	go func() {
		_ = p.Close()
		_ = p.Close() // second close must not block on a second cmd.Wait
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("creackPTY.Close did not return: double Close deadlocked on cmd.Wait")
	}
}

// TestCreackPTYResizeSignalsOnIdenticalSize guards the resize self-heal: the
// kernel only raises SIGWINCH when TIOCSWINSZ actually changes the size, so a
// re-asserted (identical) grid relies on Resize's explicit signal. A zellij
// client that lost the original update would otherwise keep its server laid
// out for a stale size forever — the "terminal doesn't repaint after resizing
// the pane" desync.
func TestCreackPTYResizeSignalsOnIdenticalSize(t *testing.T) {
	p, err := defaultSpawn(context.Background(),
		[]string{"/bin/sh", "-c", `trap 'echo WINCHED' WINCH; while :; do sleep 0.05; done`}, 0, 0)
	if err != nil {
		t.Fatalf("spawn: %v", err)
	}
	defer p.Close()

	// Give the shell a beat to install the trap, then resize twice to the SAME
	// size. The first call changes the size (fresh PTYs start at 0x0) and the
	// second is identical — only the explicit signal can deliver it.
	time.Sleep(200 * time.Millisecond)
	if err := p.Resize(24, 80); err != nil {
		t.Fatalf("resize 1: %v", err)
	}
	time.Sleep(200 * time.Millisecond)
	if err := p.Resize(24, 80); err != nil {
		t.Fatalf("resize 2: %v", err)
	}

	deadline := time.Now().Add(5 * time.Second)
	var out strings.Builder
	buf := make([]byte, 4096)
	for time.Now().Before(deadline) {
		n, err := p.Read(buf)
		if n > 0 {
			out.WriteString(string(buf[:n]))
			if strings.Count(out.String(), "WINCHED") >= 2 {
				return
			}
		}
		if err != nil {
			break
		}
	}
	t.Fatalf("expected 2 WINCHED traps (one per Resize, including the identical one), got output: %q", out.String())
}

// TestCreackPTYSpawnsAtRequestedSize: the child must see the requested grid on
// its very first TIOCGWINSZ, with no SIGWINCH involved — sizing after exec
// races the client installing its WINCH handler (a missed signal strands the
// zellij session at the previous client's size).
func TestCreackPTYSpawnsAtRequestedSize(t *testing.T) {
	p, err := defaultSpawn(context.Background(), []string{"/bin/sh", "-c", "stty size"}, 40, 140)
	if err != nil {
		t.Fatalf("spawn: %v", err)
	}
	defer p.Close()

	deadline := time.Now().Add(5 * time.Second)
	var out strings.Builder
	buf := make([]byte, 4096)
	for time.Now().Before(deadline) {
		n, readErr := p.Read(buf)
		if n > 0 {
			out.WriteString(string(buf[:n]))
			if strings.Contains(out.String(), "40 140") {
				return
			}
		}
		if readErr != nil {
			break
		}
	}
	t.Fatalf("child did not see the spawn size 40x140, got output: %q", out.String())
}

// TestCreackPTYCloseTermsBeforeKill: Close must give the attach process a
// chance to exit on SIGTERM (a zellij client deregisters from its server on
// SIGTERM; a straight SIGKILL leaves a ghost client that pins the session's
// size), and must still return promptly for a process that ignores SIGTERM.
func TestCreackPTYCloseTermsBeforeKill(t *testing.T) {
	t.Run("cooperative process exits within the grace", func(t *testing.T) {
		p, err := defaultSpawn(context.Background(),
			[]string{"/bin/sh", "-c", `trap 'exit 0' TERM; while :; do sleep 0.05; done`}, 0, 0)
		if err != nil {
			t.Fatalf("spawn: %v", err)
		}
		time.Sleep(200 * time.Millisecond) // let the trap install
		start := time.Now()
		_ = p.Close()
		if elapsed := time.Since(start); elapsed >= detachGrace {
			t.Fatalf("Close took %v: SIGTERM path did not let a cooperative process exit before the kill grace", elapsed)
		}
	})

	t.Run("TERM-ignoring process is killed after the grace", func(t *testing.T) {
		p, err := defaultSpawn(context.Background(),
			[]string{"/bin/sh", "-c", `trap '' TERM; while :; do sleep 0.05; done`}, 0, 0)
		if err != nil {
			t.Fatalf("spawn: %v", err)
		}
		time.Sleep(200 * time.Millisecond)
		done := make(chan struct{})
		go func() {
			_ = p.Close()
			close(done)
		}()
		select {
		case <-done:
		case <-time.After(5 * time.Second):
			t.Fatal("Close did not return for a TERM-ignoring process: SIGKILL fallback missing")
		}
	})
}
