package agent

import (
	"encoding/json"
	"io"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

type codexTurnDispatchCaptureWriter struct {
	writes chan []byte
}

func (writer *codexTurnDispatchCaptureWriter) Write(p []byte) (int, error) {
	copyOfP := append([]byte(nil), p...)
	writer.writes <- copyOfP
	return len(p), nil
}

func (*codexTurnDispatchCaptureWriter) Close() error { return nil }

func TestCodexAppServerTurnDispatchFencesLifecycleOnlyThroughPhysicalWrite(t *testing.T) {
	writer := &codexTurnDispatchCaptureWriter{writes: make(chan []byte, 1)}
	driver := &CodexAppServerDriver{codexRPCState: codexRPCState{
		pending:  make(map[int]pendingAppServerRequest),
		rawStdin: writer,
	}}
	driver.updateThreadSnapshot(appServerThread{
		ID:     "thread-1",
		Status: appServerThreadStatus{Type: "idle"},
	})

	// Hold only the physical writer mutex so sendTurnRequest can select/validate
	// idle and acquire the lifecycle dispatch fence, then pause before Write.
	driver.transportWriteMu.Lock()
	sendDone := make(chan error, 1)
	go func() {
		sendDone <- driver.sendTurnRequest(t.Context(), "thread-1", "turn/start", map[string]any{
			"threadId": "thread-1",
			"input":    []map[string]any{},
		})
	}()
	require.Eventually(t, func() bool {
		driver.mu.Lock()
		defer driver.mu.Unlock()
		return len(driver.pending) == 1
	}, time.Second, time.Millisecond)

	notificationStarted := make(chan struct{})
	notificationDone := make(chan struct{})
	go func() {
		close(notificationStarted)
		driver.handleNotification(appServerEnvelope{
			Method: "turn/started",
			Params: rawRPCResult(t, appServerTurnNotification{
				ThreadID: "thread-1",
				Turn: appServerTurn{
					ID:     "turn-notification",
					Status: appServerTurnStatus{Type: "inProgress"},
				},
			}),
		})
		close(notificationDone)
	}()
	<-notificationStarted
	select {
	case <-notificationDone:
		t.Fatal("turn/started crossed target validation before the turn/start frame was written")
	case <-time.After(20 * time.Millisecond):
	}
	status, _, ok := driver.currentThreadState("thread-1")
	require.True(t, ok)
	require.Equal(t, "idle", normalizeThreadStatus(status).Type)

	driver.transportWriteMu.Unlock()
	var request struct {
		ID int `json:"id"`
	}
	select {
	case payload := <-writer.writes:
		require.NoError(t, json.Unmarshal(payload, &request))
	case <-time.After(time.Second):
		t.Fatal("turn/start frame was not written")
	}
	require.NotZero(t, request.ID)

	// The dispatch fence must be released after Write, not after the response;
	// otherwise a notification-before-response stdout sequence deadlocks.
	select {
	case <-notificationDone:
	case <-time.After(time.Second):
		t.Fatal("turn/started remained blocked while request waited for its response")
	}
	status, activeTurnID, ok := driver.currentThreadState("thread-1")
	require.True(t, ok)
	require.Equal(t, "active", normalizeThreadStatus(status).Type)
	require.Equal(t, "turn-notification", activeTurnID)

	driver.mu.Lock()
	pending, exists := driver.pending[request.ID]
	delete(driver.pending, request.ID)
	driver.mu.Unlock()
	require.True(t, exists)
	pending.responseCh <- appServerEnvelope{Result: rawRPCResult(t, appServerTurnResult{
		Turn: appServerTurn{ID: "turn-response", Status: appServerTurnStatus{Type: "inProgress"}},
	})}
	select {
	case err := <-sendDone:
		require.NoError(t, err)
	case <-time.After(time.Second):
		t.Fatal("turn/start did not finish after its response")
	}
	require.Equal(t, "turn-notification", driver.currentActiveTurnID(), "response acknowledgement must not overwrite newer authoritative lifecycle state")
}

var _ io.WriteCloser = (*codexTurnDispatchCaptureWriter)(nil)
