package agent

import (
	"bufio"
	"context"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

func TestOpenCodeSpawnWaitDrainsTerminalBoundaryBeforeReturning(t *testing.T) {
	workDir := t.TempDir()
	recordDir := t.TempDir()
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	driver := &OpenCodeDriver{}
	spawnDone := make(chan openCodeSpawnAttempt, 1)
	go func() {
		result, err := driver.Spawn(ctx, drivercontract.LaunchConfig{
			AgentID: "agent-output-drain",
			Binary:  writeFakeOpenCodeServeBinary(t),
			WorkDir: workDir,
		})
		spawnDone <- openCodeSpawnAttempt{result: result, err: err}
	}()

	waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "event_headers_flushed", spawnDone)
	writeFakeOpenCodeMarker("release_event_data")
	done := receiveOpenCodeSpawnAttempt(t, ctx, spawnDone)
	require.NoError(t, done.err)

	// Model an asynchronous native-summarize failure. The terminal handler
	// cancels serve before enqueueing Error+TurnEnd, so process exit can win the
	// transport race while stdout's reader is deliberately delayed.
	driver.mu.Lock()
	driver.summarizeInProgress = true
	driver.sessionStatus = "busy"
	driver.turnActive = true
	driver.mu.Unlock()
	driver.handleSSEEvent(`{"type":"session.error","properties":{"sessionID":"sess-1","error":{"name":"ProviderError","message":"compaction provider failed"}}}`)

	waitDone := make(chan error, 1)
	go func() { waitDone <- done.result.Wait() }()
	select {
	case err := <-waitDone:
		t.Fatalf("Wait returned before the delayed stdout consumer drained terminal events: %v", err)
	case <-time.After(50 * time.Millisecond):
	}

	payloadDone := make(chan string, 1)
	terminalHandled := make(chan struct{})
	allowEOFRead := make(chan struct{})
	go func() {
		reader := bufio.NewReader(done.result.Stdout)
		var payload strings.Builder
		for {
			line, err := reader.ReadString('\n')
			payload.WriteString(line)
			if strings.Contains(line, `"kind":"turn_end"`) {
				close(terminalHandled)
				// Model Process.handleParsedEvent completing before
				// parseStdout asks for the read that proves EOF.
				<-allowEOFRead
			}
			if err != nil {
				payloadDone <- payload.String()
				return
			}
		}
	}()
	select {
	case <-terminalHandled:
	case <-ctx.Done():
		t.Fatalf("translated terminal boundary was not delivered before the test context ended: %v", ctx.Err())
	}
	select {
	case err := <-waitDone:
		t.Fatalf("Wait returned before parseStdout requested EOF after handling TurnEnd: %v", err)
	case <-time.After(50 * time.Millisecond):
	}
	close(allowEOFRead)
	select {
	case err := <-waitDone:
		require.Error(t, err, "the fake serve process is canceled by retirement")
	case <-ctx.Done():
		t.Fatalf("Wait did not return after stdout drained before the test context ended: %v", ctx.Err())
	}
	var text string
	select {
	case text = <-payloadDone:
	case <-ctx.Done():
		t.Fatalf("translated stdout did not close after normal drain before the test context ended: %v", ctx.Err())
	}
	require.Contains(t, text, `"kind":"session_init"`)
	require.Contains(t, text, `"kind":"error"`)
	require.Contains(t, text, `"kind":"turn_end"`)
	require.Less(t, strings.Index(text, `"kind":"error"`), strings.Index(text, `"kind":"turn_end"`))
	require.Empty(t, driver.translatedLines, "normal Wait must not strand buffered lifecycle events")
}

func TestOpenCodeSpawnWaitsForSSEBeforeInitialPrompt(t *testing.T) {
	workDir := t.TempDir()
	recordDir := t.TempDir()
	binary := writeFakeOpenCodeServeBinary(t)
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	spawnDone := make(chan openCodeSpawnAttempt, 1)
	driver := &OpenCodeDriver{}
	go func() {
		result, err := driver.Spawn(ctx, testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-sse-ready", Binary: binary, WorkDir: workDir}, testLaunchSurfaces{Prompt: "hello"}))
		spawnDone <- openCodeSpawnAttempt{result: result, err: err}
	}()

	waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "event_headers_flushed", spawnDone)
	select {
	case done := <-spawnDone:
		if done.err == nil {
			done.result.Terminate()
			_ = done.result.Wait()
		}
		t.Fatalf("Spawn returned before first SSE data frame was released: %v", done.err)
	default:
	}
	require.False(t, fileExists(filepath.Join(recordDir, "prompt_before_event")))
	require.False(t, fileExists(filepath.Join(recordDir, "prompt_after_event")))

	writeFakeOpenCodeMarker("release_event_data")
	done := receiveOpenCodeSpawnAttempt(t, ctx, spawnDone)
	require.NoError(t, done.err)
	result := done.result
	driver.mu.Lock()
	serverURL := driver.serverURL
	password := driver.password
	serverCancel := driver.serverCancel
	driver.mu.Unlock()
	require.NotEmpty(t, serverURL)
	require.NotEmpty(t, password)
	require.NotNil(t, serverCancel)
	require.NotNil(t, result.Cmd)
	t.Cleanup(func() {
		result.Terminate()
		waitCh := make(chan struct{})
		go func() {
			_ = result.Wait()
			close(waitCh)
		}()
		select {
		case <-waitCh:
		case <-time.After(2 * time.Second):
			t.Fatal("fake opencode serve did not exit")
		}
	})

	require.Eventually(t, func() bool {
		return fileExists(filepath.Join(recordDir, "prompt_after_event")) ||
			fileExists(filepath.Join(recordDir, "prompt_before_event"))
	}, 2*time.Second, 10*time.Millisecond)
	require.False(t, fileExists(filepath.Join(recordDir, "prompt_before_event")))
	require.True(t, fileExists(filepath.Join(recordDir, "prompt_after_event")))
}

func TestOpenCodeSpawnSessionFailureClearsServerState(t *testing.T) {
	workDir := t.TempDir()
	recordDir := t.TempDir()
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_SESSION_FAILURE", "1")

	// Keep enough headroom for the fake serve process to pass health under the
	// full desktop package's parallel test load before exercising session error
	// cleanup.
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	driver := &OpenCodeDriver{}
	_, err := driver.Spawn(ctx, drivercontract.LaunchConfig{
		AgentID: "agent-session-failure",
		Binary:  writeFakeOpenCodeServeBinary(t),
		WorkDir: workDir,
	})

	require.ErrorContains(t, err, "opencode: create session:")
	assertOpenCodeServerStateCleared(t, driver)
}

func TestOpenCodeSpawnInitialPromptFailureRetiresServer(t *testing.T) {
	tests := []struct {
		name        string
		prompt      string
		attachments []types.RuntimeAttachment
		failureMode string
	}{
		{
			name: "unsupported attachments only",
			attachments: []types.RuntimeAttachment{{
				Filename: "manual.pdf",
				MIME:     "application/pdf",
				URI:      "file:///tmp/manual.pdf",
			}},
		},
		{
			name:        "definitive prompt rejection",
			prompt:      "hello",
			failureMode: "status",
		},
		{
			name:        "ambiguous prompt connection close",
			prompt:      "hello",
			failureMode: "eof",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			workDir := t.TempDir()
			recordDir := t.TempDir()
			t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
			t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_PROMPT_FAILURE", tt.failureMode)

			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			spawnDone := make(chan openCodeSpawnAttempt, 1)
			driver := &OpenCodeDriver{}
			binary := writeFakeOpenCodeServeBinary(t)
			go func() {
				result, err := driver.Spawn(ctx, testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-initial-prompt-failure", Binary: binary, WorkDir: workDir, PromptAttachments: tt.attachments}, testLaunchSurfaces{Prompt: tt.prompt}))
				spawnDone <- openCodeSpawnAttempt{result: result, err: err}
			}()

			waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "event_headers_flushed", spawnDone)
			writeFakeOpenCodeMarker("release_event_data")
			done := receiveOpenCodeSpawnAttempt(t, ctx, spawnDone)
			require.Error(t, done.err)
			require.Contains(t, done.err.Error(), "opencode: initial prompt:")
			assertOpenCodeServerStateCleared(t, driver)
			driver.mu.Lock()
			require.Nil(t, driver.promptAwaitingRunner)
			require.False(t, driver.turnActive)
			require.Empty(t, driver.sessionStatus)
			require.True(t, driver.sseDeliveryBlocked)
			driver.mu.Unlock()
		})
	}
}

func TestOpenCodeSpawnResumeBootstrap(t *testing.T) {
	tests := []struct {
		name             string
		tailRole         string
		prompt           string
		wantTailLookup   bool
		wantTailKick     bool
		wantPromptAccept bool
		wantSpawnError   bool
		blockTailKick    bool
		pendingRevert    bool
	}{
		{
			name:             "recovers interrupted tail after SSE ready",
			tailRole:         "user",
			wantTailLookup:   true,
			wantTailKick:     true,
			wantPromptAccept: true,
			blockTailKick:    true,
		},
		{
			name:             "new turn input owns resume",
			tailRole:         "user",
			prompt:           "hello",
			wantTailLookup:   true,
			wantPromptAccept: true,
		},
		{
			name:           "pending revert suppresses stale tail bootstrap",
			tailRole:       "user",
			wantTailLookup: true,
			pendingRevert:  true,
		},
		{
			name:           "tail lookup failure fails spawn",
			tailRole:       "failure",
			wantTailLookup: true,
			wantSpawnError: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			workDir := t.TempDir()
			recordDir := t.TempDir()
			binary := writeFakeOpenCodeServeBinary(t)
			t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
			t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RESUME_TAIL_ROLE", tt.tailRole)
			if tt.pendingRevert {
				t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_PENDING_REVERT", "1")
			}
			if tt.blockTailKick {
				t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_BLOCK_RESUME_PROMPT", "1")
			}

			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()

			spawnDone := make(chan openCodeSpawnAttempt, 1)
			driver := &OpenCodeDriver{}
			go func() {
				result, err := driver.Spawn(ctx, testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-resume-bootstrap", Binary: binary, WorkDir: workDir, SessionID: "sess-1"}, testLaunchSurfaces{Prompt: tt.prompt}))
				spawnDone <- openCodeSpawnAttempt{result: result, err: err}
			}()

			waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "event_headers_flushed", spawnDone)
			require.False(t, fileExists(filepath.Join(recordDir, "resume_messages_before_event")))
			require.False(t, fileExists(filepath.Join(recordDir, "resume_messages_after_event")))
			require.False(t, fileExists(filepath.Join(recordDir, "resume_tail_kicked")))

			writeFakeOpenCodeMarker("release_event_data")
			if tt.blockTailKick {
				waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "resume_tail_kicked", spawnDone)
				driver.mu.Lock()
				translatedLines := driver.translatedLines
				driver.mu.Unlock()
				select {
				case line := <-translatedLines:
					events := driver.ParseLine(line)
					require.Len(t, events, 1)
					require.Equal(t, consts.ParsedEventKindSessionInit, events[0].Kind)
				case <-time.After(time.Second):
					t.Fatal("session_init was not enqueued before resume bootstrap")
				}
				writeFakeOpenCodeMarker("release_resume_prompt")
			}
			done := receiveOpenCodeSpawnAttempt(t, ctx, spawnDone)
			if tt.wantSpawnError {
				require.ErrorContains(t, done.err, "resume prompt bootstrap")
				require.Equal(t, tt.wantTailLookup, fileExists(filepath.Join(recordDir, "resume_messages_after_event")))
				require.False(t, fileExists(filepath.Join(recordDir, "resume_tail_kicked")))
				require.False(t, fileExists(filepath.Join(recordDir, "prompt_after_event")))
				assertOpenCodeServerStateCleared(t, driver)
				return
			}
			require.NoError(t, done.err)
			result := done.result
			t.Cleanup(func() {
				result.Terminate()
				waitCh := make(chan struct{})
				go func() {
					_ = result.Wait()
					close(waitCh)
				}()
				select {
				case <-waitCh:
				case <-time.After(2 * time.Second):
					t.Fatal("fake opencode serve did not exit")
				}
			})

			require.Equal(t, tt.wantTailLookup, fileExists(filepath.Join(recordDir, "resume_messages_after_event")))
			require.False(t, fileExists(filepath.Join(recordDir, "resume_messages_before_event")))
			require.Equal(t, tt.wantTailKick, fileExists(filepath.Join(recordDir, "resume_tail_kicked")))
			if tt.wantPromptAccept {
				require.Eventually(t, func() bool {
					return fileExists(filepath.Join(recordDir, "prompt_after_event"))
				}, time.Second, 10*time.Millisecond)
			} else {
				require.False(t, fileExists(filepath.Join(recordDir, "prompt_after_event")))
			}
		})
	}
}

func TestOpenCodeSpawnSSEReadyReportsServeExit(t *testing.T) {
	workDir := t.TempDir()
	recordDir := t.TempDir()
	binary := writeFakeOpenCodeServeBinary(t)
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_EXIT_ON_EVENT", "1")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	startedAt := time.Now()
	driver := &OpenCodeDriver{}
	_, err := driver.Spawn(ctx, testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-sse-exit", Binary: binary, WorkDir: workDir}, testLaunchSurfaces{Prompt: "hello"}))

	require.Error(t, err)
	require.Contains(t, err.Error(), "opencode: subscribe SSE: serve process exited before SSE subscription became ready")
	require.Contains(t, err.Error(), "fake opencode event startup failure")
	serveLog, readErr := os.ReadFile(openCodeServeLogPath(workDir))
	require.NoError(t, readErr)
	require.Contains(t, string(serveLog), "[stderr] fake opencode event startup failure")
	require.Less(t, time.Since(startedAt), 4*time.Second)
	assertOpenCodeServerStateCleared(t, driver)
}

func TestOpenCodeLaunchPublishesCancelOnlyAfterHealth(t *testing.T) {
	t.Run("agent", func(t *testing.T) {
		recordDir := t.TempDir()
		t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
		t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_BLOCK_HEALTH", "1")
		driver := &OpenCodeDriver{}
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		binary := writeFakeOpenCodeServeBinary(t)
		workDir := t.TempDir()
		spawnDone := make(chan openCodeSpawnAttempt, 1)
		go func() {
			result, err := driver.Spawn(ctx, drivercontract.LaunchConfig{
				AgentID: "agent-blocked-health",
				Binary:  binary,
				WorkDir: workDir,
			})
			spawnDone <- openCodeSpawnAttempt{result: result, err: err}
		}()

		waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "health_requested", spawnDone)
		driver.mu.Lock()
		cancelBeforeHealth := driver.serverCancel
		driver.mu.Unlock()
		require.Nil(t, cancelBeforeHealth)

		writeFakeOpenCodeMarker("release_health")
		waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "event_headers_flushed", spawnDone)
		driver.mu.Lock()
		cancelAfterHealth := driver.serverCancel
		driver.mu.Unlock()
		require.NotNil(t, cancelAfterHealth)

		writeFakeOpenCodeMarker("release_event_data")
		done := receiveOpenCodeSpawnAttempt(t, ctx, spawnDone)
		require.NoError(t, done.err)
		done.result.Terminate()
		_ = done.result.Wait()
		assertOpenCodeServerStateCleared(t, driver)
	})

	t.Run("detached", func(t *testing.T) {
		recordDir := t.TempDir()
		t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
		t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_BLOCK_HEALTH", "1")
		driver := &OpenCodeDriver{}
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		binary := writeFakeOpenCodeServeBinary(t)
		workDir := t.TempDir()
		type result struct {
			cleanup func()
			err     error
		}
		started := make(chan result, 1)
		go func() {
			cleanup, err := driver.startTemporaryServer(ctx, drivercontract.LaunchConfig{
				AgentID: "detached-blocked-health",
				Binary:  binary,
				WorkDir: workDir,
			})
			started <- result{cleanup: cleanup, err: err}
		}()

		waitForOpenCodeMarker(t, recordDir, "health_requested")
		driver.mu.Lock()
		urlDuringHealth := driver.serverURL
		passwordDuringHealth := driver.password
		cancelDuringHealth := driver.serverCancel
		driver.mu.Unlock()
		require.NotEmpty(t, urlDuringHealth)
		require.NotEmpty(t, passwordDuringHealth)
		require.Nil(t, cancelDuringHealth)

		writeFakeOpenCodeMarker("release_health")
		var done result
		select {
		case done = <-started:
		case <-time.After(2 * time.Second):
			t.Fatal("detached server did not become healthy")
		}
		require.NoError(t, done.err)
		require.NotNil(t, done.cleanup)
		driver.mu.Lock()
		urlWhileRunning := driver.serverURL
		passwordWhileRunning := driver.password
		cancelWhileRunning := driver.serverCancel
		driver.mu.Unlock()
		require.NotEmpty(t, urlWhileRunning)
		require.NotEmpty(t, passwordWhileRunning)
		require.Nil(t, cancelWhileRunning)
		var callers sync.WaitGroup
		for range 8 {
			callers.Add(1)
			go func() {
				defer callers.Done()
				done.cleanup()
			}()
		}
		callers.Wait()
		assertOpenCodeServerStateCleared(t, driver)
	})
}

func TestOpenCodeSpawnTerminateIsIdempotent(t *testing.T) {
	recordDir := t.TempDir()
	t.Setenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR", recordDir)
	driver := &OpenCodeDriver{}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	binary := writeFakeOpenCodeServeBinary(t)
	workDir := t.TempDir()
	spawnDone := make(chan openCodeSpawnAttempt, 1)
	go func() {
		result, err := driver.Spawn(ctx, drivercontract.LaunchConfig{
			AgentID: "agent-repeat-terminate",
			Binary:  binary,
			WorkDir: workDir,
		})
		spawnDone <- openCodeSpawnAttempt{result: result, err: err}
	}()
	waitForOpenCodeMarkerOrSpawnExit(t, ctx, recordDir, "event_headers_flushed", spawnDone)
	writeFakeOpenCodeMarker("release_event_data")
	done := receiveOpenCodeSpawnAttempt(t, ctx, spawnDone)
	require.NoError(t, done.err)

	var callers sync.WaitGroup
	for range 8 {
		callers.Add(1)
		go func() {
			defer callers.Done()
			done.result.Terminate()
		}()
	}
	callers.Wait()
	waited := make(chan struct{})
	go func() {
		_ = done.result.Wait()
		close(waited)
	}()
	select {
	case <-waited:
	case <-time.After(2 * time.Second):
		t.Fatal("repeated Terminate calls did not stop the server")
	}
	assertOpenCodeServerStateCleared(t, driver)
}

type openCodeSpawnAttempt struct {
	result drivercontract.SpawnResult
	err    error
}

func receiveOpenCodeSpawnAttempt(t *testing.T, ctx context.Context, spawnDone <-chan openCodeSpawnAttempt) openCodeSpawnAttempt {
	t.Helper()
	// Spawn owns the deadline through its test context. Once the fixture has
	// released the awaited phase, synchronize on its terminal signal rather
	// than imposing a second, shorter wall-clock deadline.
	select {
	case done := <-spawnDone:
		return done
	case <-ctx.Done():
		t.Fatalf("Spawn did not return before its test context ended: %v", ctx.Err())
		return openCodeSpawnAttempt{}
	}
}

func waitForOpenCodeMarkerOrSpawnExit(t *testing.T, ctx context.Context, recordDir string, marker string, spawnDone <-chan openCodeSpawnAttempt) {
	t.Helper()

	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()
	// The fake server runs in a subprocess, so marker files are its rendezvous
	// signal. Spawn's context remains the single watchdog: it guarantees a
	// terminal spawnDone result if the expected marker never arrives.
	for {
		if fileExists(filepath.Join(recordDir, marker)) {
			return
		}
		select {
		case done := <-spawnDone:
			if done.err == nil {
				done.result.Terminate()
				_ = done.result.Wait()
			}
			t.Fatalf("Spawn returned before %s marker: %v", marker, done.err)
		case <-ctx.Done():
			t.Fatalf("fake opencode marker %s was not written before the test context ended: %v", marker, ctx.Err())
		case <-ticker.C:
		}
	}
}

func waitForOpenCodeMarker(t *testing.T, recordDir string, marker string) {
	t.Helper()
	require.Eventually(t, func() bool {
		return fileExists(filepath.Join(recordDir, marker))
	}, 2*time.Second, 10*time.Millisecond, "fake opencode marker %s was not written", marker)
}

func assertOpenCodeServerStateCleared(t *testing.T, driver *OpenCodeDriver) {
	t.Helper()
	driver.mu.Lock()
	defer driver.mu.Unlock()
	require.Empty(t, driver.serverURL)
	require.Empty(t, driver.password)
	require.Nil(t, driver.serverCancel)
}
