package terminal

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

// testShell 返回各平台一个可交互读 stdin 的 shell。
func testShell() string {
	if runtime.GOOS == "windows" {
		return "cmd"
	}
	return "/bin/sh"
}

func newTestSup(t *testing.T, max int) *Supervisor {
	t.Helper()
	sup := NewSupervisor(SupervisorConfig{
		Shell:       testShell(),
		MaxSessions: max,
		KillGrace:   2 * time.Second,
		EnvWhitelist: []string{
			"PATH", "HOME", "TERM",
			"SystemRoot", "TEMP", "TMP", "USERPROFILE", "ComSpec", "PATHEXT",
		},
	}, nil)
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		sup.CloseAll(ctx)
	})
	return sup
}

// readUntil 在独立 goroutine 中持续读会话输出，直到累计内容包含 marker 或超时。
// 阻塞的 Read goroutine 会在会话被关闭（defer/Cleanup）后随 Read 报错而退出。
func readUntil(sess *Session, marker string, d time.Duration) string {
	out := make(chan []byte, 16)
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := sess.Read(buf)
			if n > 0 {
				b := make([]byte, n)
				copy(b, buf[:n])
				out <- b
			}
			if err != nil {
				close(out)
				return
			}
		}
	}()
	deadline := time.After(d)
	var acc strings.Builder
	for {
		select {
		case b, ok := <-out:
			if !ok {
				return acc.String()
			}
			acc.Write(b)
			if strings.Contains(acc.String(), marker) {
				return acc.String()
			}
		case <-deadline:
			return acc.String()
		}
	}
}

// TestOpen_ReadsShellOutput 验证开会话后向 shell 写命令能读回其输出。
// Unix 走真 PTY；Windows 走降级管道实现。
func TestOpen_ReadsShellOutput(t *testing.T) {
	sup := newTestSup(t, 5)
	sess, err := sup.Open(OpenParams{})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	defer sup.Close(ctx, sess.ID)

	const marker = "ACWMARK42"
	cmd := "echo " + marker + "\n"
	if runtime.GOOS == "windows" {
		cmd = "echo " + marker + "\r\n"
	}
	if _, err := sess.Write([]byte(cmd)); err != nil {
		t.Fatalf("write: %v", err)
	}

	got := readUntil(sess, marker, 5*time.Second)
	if !strings.Contains(got, marker) {
		t.Fatalf("marker %q not found in shell output; got %q", marker, got)
	}
}

// TestOpen_MaxSessions 验证达到上限后再开会话返回 ErrTooManySessions。
func TestOpen_MaxSessions(t *testing.T) {
	sup := newTestSup(t, 1)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	s1, err := sup.Open(OpenParams{})
	if err != nil {
		t.Fatalf("first open: %v", err)
	}
	defer sup.Close(ctx, s1.ID)

	if _, err := sup.Open(OpenParams{}); !errors.Is(err, ErrTooManySessions) {
		t.Fatalf("expected ErrTooManySessions, got %v", err)
	}
	if got := sup.Count(); got != 1 {
		t.Fatalf("expected 1 active session, got %d", got)
	}
}

// TestClose_TerminatesSession 验证 Close 终止 shell（done 关闭）并从映射移除。
func TestClose_TerminatesSession(t *testing.T) {
	sup := newTestSup(t, 5)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	sess, err := sup.Open(OpenParams{})
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	if got := sup.Count(); got != 1 {
		t.Fatalf("expected 1 active session, got %d", got)
	}

	sup.Close(ctx, sess.ID)

	select {
	case <-sess.Done():
	case <-time.After(8 * time.Second):
		t.Fatal("session not terminated within 8s after Close")
	}

	// monitor 在 done 关闭后异步移除映射，轮询直至归零。
	deadline := time.Now().Add(3 * time.Second)
	for sup.Count() != 0 {
		if time.Now().After(deadline) {
			t.Fatalf("session not removed from supervisor; count=%d", sup.Count())
		}
		time.Sleep(20 * time.Millisecond)
	}
}

// TestCloseAll_ClosesEverySession 验证 CloseAll 终止全部会话。
func TestCloseAll_ClosesEverySession(t *testing.T) {
	sup := newTestSup(t, 5)
	for i := 0; i < 3; i++ {
		if _, err := sup.Open(OpenParams{}); err != nil {
			t.Fatalf("open %d: %v", i, err)
		}
	}
	if got := sup.Count(); got != 3 {
		t.Fatalf("expected 3 active sessions, got %d", got)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
	defer cancel()
	sup.CloseAll(ctx)

	deadline := time.Now().Add(3 * time.Second)
	for sup.Count() != 0 {
		if time.Now().After(deadline) {
			t.Fatalf("sessions not all closed; count=%d", sup.Count())
		}
		time.Sleep(20 * time.Millisecond)
	}
}
