package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/require"

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

func openCodeMessageIDFromEncoded(encoded uint64, suffix string) string {
	return fmt.Sprintf("%s%012x%s", openCodeMessageIDPrefix, encoded, suffix)
}

func TestOpenCodeCreateSessionPinsDefaultAgent(t *testing.T) {
	var body struct {
		Agent string `json:"agent"`
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "/session", r.URL.Path)
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"id": "sess-created"}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{serverURL: server.URL, password: "pw", httpClient: server.Client()}
	sessionID, resumed, pendingRevert, err := driver.createOrResumeSession(
		context.Background(),
		drivercontract.LaunchConfig{WorkDir: t.TempDir()},
	)
	require.NoError(t, err)
	require.Equal(t, "sess-created", sessionID)
	require.False(t, resumed)
	require.False(t, pendingRevert)
	require.Equal(t, openCodeDefaultAgent, body.Agent)
}

func TestOpenCodeResumeSessionRejectsDifferentAgentWithoutFreshFallback(t *testing.T) {
	var requests []string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests = append(requests, r.Method+" "+r.URL.Path)
		if r.Method == http.MethodGet && r.URL.Path == "/session/sess-old" {
			require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"id": "sess-old", "agent": "other"}))
			return
		}
		t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
	}))
	defer server.Close()

	driver := &OpenCodeDriver{serverURL: server.URL, password: "pw", httpClient: server.Client()}
	_, _, _, err := driver.createOrResumeSession(context.Background(), drivercontract.LaunchConfig{
		SessionID: "sess-old",
		WorkDir:   t.TempDir(),
	})
	require.ErrorContains(t, err, "not resumable")
	require.Equal(t, []string{"GET /session/sess-old"}, requests)
	require.Nil(t, driver.sessionResumeFallback)
}

func TestOpenCodeResumeSessionNotFoundFallsBackOnce(t *testing.T) {
	var requests []string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests = append(requests, r.Method+" "+r.URL.Path)
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/session/sess-old":
			w.WriteHeader(http.StatusNotFound)
		case r.Method == http.MethodPost && r.URL.Path == "/session":
			require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"id": "sess-created"}))
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{serverURL: server.URL, password: "pw", httpClient: server.Client()}
	sessionID, resumed, pendingRevert, err := driver.createOrResumeSession(context.Background(), drivercontract.LaunchConfig{
		SessionID: "sess-old",
		WorkDir:   t.TempDir(),
	})
	require.NoError(t, err)
	require.Equal(t, "sess-created", sessionID)
	require.False(t, resumed)
	require.False(t, pendingRevert)
	require.Equal(t, []string{"GET /session/sess-old", "POST /session"}, requests)
	require.Equal(t, &drivercontract.SessionResumeFallback{
		Reason:    "invalid_or_not_found_session",
		Operation: "session/get",
	}, driver.sessionResumeFallback)
}

func TestOpenCodeResumeSessionTransportFailureDoesNotFreshFallback(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet && r.URL.Path == "/session/sess-old" {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
	}))
	defer server.Close()

	driver := &OpenCodeDriver{serverURL: server.URL, password: "pw", httpClient: server.Client()}
	_, _, _, err := driver.createOrResumeSession(context.Background(), drivercontract.LaunchConfig{
		SessionID: "sess-old",
		WorkDir:   t.TempDir(),
	})
	require.Error(t, err)
	require.Nil(t, driver.sessionResumeFallback)
}

func TestOpenCodeResumeSessionReusesDefaultAgentSession(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/session/sess-current", r.URL.Path)
		require.NoError(t, json.NewEncoder(w).Encode(map[string]string{
			"id": "sess-current", "agent": openCodeDefaultAgent,
		}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{serverURL: server.URL, password: "pw", httpClient: server.Client()}
	sessionID, resumed, pendingRevert, err := driver.createOrResumeSession(context.Background(), drivercontract.LaunchConfig{
		SessionID: "sess-current",
		WorkDir:   t.TempDir(),
	})
	require.NoError(t, err)
	require.Equal(t, "sess-current", sessionID)
	require.True(t, resumed)
	require.False(t, pendingRevert)
}

func TestOpenCodeResumeSessionReportsPendingRevert(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/session/sess-current", r.URL.Path)
		require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
			"id":    "sess-current",
			"agent": openCodeDefaultAgent,
			"revert": map[string]any{
				"messageID": "msg_100000000001AAAAAAAAAAAAAA",
			},
		}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{serverURL: server.URL, password: "pw", httpClient: server.Client()}
	sessionID, resumed, pendingRevert, err := driver.createOrResumeSession(context.Background(), drivercontract.LaunchConfig{
		SessionID: "sess-current",
		WorkDir:   t.TempDir(),
	})
	require.NoError(t, err)
	require.Equal(t, "sess-current", sessionID)
	require.True(t, resumed)
	require.True(t, pendingRevert)
}

func TestOpenCodeRecoverInterruptedResumedSessionKicksTailUserMessage(t *testing.T) {
	workDir := t.TempDir()
	tailUserID := openCodeMessageIDFromEncoded(0x100000000001, "AAAAAAAAAAAAAA")
	var requests []string
	var promptBody struct {
		Agent     string           `json:"agent"`
		MessageID string           `json:"messageID"`
		Parts     []map[string]any `json:"parts"`
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests = append(requests, r.Method+" "+r.URL.RequestURI())
		require.Equal(t, workDir, r.Header.Get("x-opencode-directory"))
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/session/sess-current/message":
			require.Equal(t, "100", r.URL.Query().Get("limit"))
			require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
				"info": map[string]string{"id": tailUserID, "role": "user"},
			}}))
		case r.Method == http.MethodPost && r.URL.Path == "/session/sess-current/prompt_async":
			require.NoError(t, json.NewDecoder(r.Body).Decode(&promptBody))
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: workDir,
	}
	require.NoError(t, driver.recoverInterruptedResumedSession(context.Background(), "sess-current"))
	require.Equal(t, []string{
		"GET /session/sess-current/message?limit=100",
		"POST /session/sess-current/prompt_async",
	}, requests)
	require.Equal(t, openCodeDefaultAgent, promptBody.Agent)
	require.Equal(t, tailUserID, promptBody.MessageID)
	require.Empty(t, promptBody.Parts)
}

func TestOpenCodeRecoverInterruptedResumedAutomaticCompactionBindsRunnerChain(t *testing.T) {
	workDir := t.TempDir()
	compactionUserID := openCodeMessageIDFromEncoded(0x100000000001, "AAAAAAAAAAAAAA")
	summaryAssistantID := openCodeMessageIDFromEncoded(0x100000000002, "BBBBBBBBBBBBBB")
	continuationUserID := openCodeMessageIDFromEncoded(0x100000000003, "CCCCCCCCCCCCCC")
	continuationAssistantID := openCodeMessageIDFromEncoded(0x100000000004, "DDDDDDDDDDDDDD")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/session/sess-current/message":
			require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{
				{
					"info": map[string]any{"id": compactionUserID, "role": "user"},
					"parts": []map[string]any{{
						"id": "part-compaction", "messageID": compactionUserID,
						"type": "compaction", "auto": true,
					}},
				},
				{
					// A crash can leave the summary row itself unfinished. The
					// bootstrap root still comes from its persisted parent task.
					"info": map[string]any{
						"id": summaryAssistantID, "parentID": compactionUserID,
						"role": "assistant", "summary": true,
					},
				},
			}))
		case r.Method == http.MethodPost && r.URL.Path == "/session/sess-current/prompt_async":
			var body struct {
				MessageID string           `json:"messageID"`
				Parts     []map[string]any `json:"parts"`
			}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			require.Equal(t, compactionUserID, body.MessageID)
			require.Empty(t, body.Parts)
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: workDir,
		translatedLines: make(chan string, 4),
	}
	require.NoError(t, driver.recoverInterruptedResumedSession(context.Background(), "sess-current"))
	require.NotNil(t, driver.promptAwaitingRunner)
	require.Equal(t, compactionUserID, driver.autoCompactionUserMessageID)
	require.Equal(t, true, driver.runnerCompactionUserMessageIDs[compactionUserID])

	// On a fresh server, runAgentTeam reads the already-persisted compaction task but
	// does not republish its part. The resume reservation must therefore carry
	// the source binding across busy into the summary and continuation chain.
	driver.handleSSEEvent(openCodeSessionStatusEvent(t, "sess-current", "busy"))
	driver.handleSSEEvent(openCodeSummaryAssistantEvent(
		t,
		"sess-current",
		summaryAssistantID,
		compactionUserID,
	))
	driver.handleSSEEvent(`{"type":"message.part.delta","properties":{"sessionID":"sess-current","messageID":"` +
		summaryAssistantID + `","partID":"part-summary","field":"text","delta":"INTERNAL SUMMARY"}}`)
	driver.handleSSEEvent(openCodeUserMessageUpdatedEvent(t, "sess-current", continuationUserID))
	driver.handleSSEEvent(openCodeSessionCompactedEvent(t, "sess-current"))
	driver.handleSSEEvent(openCodeAssistantMessageUpdatedEvent(
		t,
		"sess-current",
		continuationAssistantID,
		continuationUserID,
	))
	driver.handleSSEEvent(`{"type":"message.part.delta","properties":{"sessionID":"sess-current","messageID":"` +
		continuationAssistantID + `","partID":"part-continuation","field":"text","delta":"VISIBLE CONTINUATION"}}`)
	driver.handleSSEEvent(openCodeSessionStatusEvent(t, "sess-current", "idle"))

	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind: consts.ParsedEventKindText,
		Text: "VISIBLE CONTINUATION",
	}}, driver.ParseLine(<-driver.translatedLines))
	require.Equal(t, consts.ParsedEventKindTurnEnd, driver.ParseLine(<-driver.translatedLines)[0].Kind)
	require.Empty(t, driver.translatedLines)
	require.False(t, driver.generationRetiring)
}

func TestOpenCodeRecoverInterruptedResumedManualCompactionBindsSummary(t *testing.T) {
	compactionUserID := openCodeMessageIDFromEncoded(0x100000000001, "AAAAAAAAAAAAAA")
	summaryAssistantID := openCodeMessageIDFromEncoded(0x100000000002, "BBBBBBBBBBBBBB")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/session/sess-current/message":
			require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
				"info": map[string]any{"id": compactionUserID, "role": "user"},
				"parts": []map[string]any{{
					"id": "part-compaction", "messageID": compactionUserID,
					"type": "compaction", "auto": false,
				}},
			}}))
		case r.Method == http.MethodPost && r.URL.Path == "/session/sess-current/prompt_async":
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
		translatedLines: make(chan string, 2),
	}
	require.NoError(t, driver.recoverInterruptedResumedSession(context.Background(), "sess-current"))
	require.Equal(t, false, driver.runnerCompactionUserMessageIDs[compactionUserID])

	driver.handleSSEEvent(openCodeSessionStatusEvent(t, "sess-current", "busy"))
	driver.handleSSEEvent(openCodeSummaryAssistantEvent(
		t,
		"sess-current",
		summaryAssistantID,
		compactionUserID,
	))
	driver.handleSSEEvent(`{"type":"message.part.delta","properties":{"sessionID":"sess-current","messageID":"` +
		summaryAssistantID + `","partID":"part-summary","field":"text","delta":"INTERNAL SUMMARY"}}`)
	driver.handleSSEEvent(openCodeSessionCompactedEvent(t, "sess-current"))
	driver.handleSSEEvent(openCodeSessionStatusEvent(t, "sess-current", "idle"))

	require.Equal(t, consts.ParsedEventKindTurnEnd, driver.ParseLine(<-driver.translatedLines)[0].Kind)
	require.Empty(t, driver.translatedLines)
	require.False(t, driver.generationRetiring)
}

func TestOpenCodeRecoverInterruptedResumedCompactionRequiresAutoMode(t *testing.T) {
	compactionUserID := openCodeMessageIDFromEncoded(0x100000000001, "AAAAAAAAAAAAAA")
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests++
		require.Equal(t, http.MethodGet, r.Method)
		require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
			"info":  map[string]any{"id": compactionUserID, "role": "user"},
			"parts": []map[string]any{{"type": "compaction"}},
		}}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	err := driver.recoverInterruptedResumedSession(context.Background(), "sess-current")
	require.Error(t, err)
	require.Contains(t, err.Error(), "missing its auto mode")
	require.Equal(t, 1, requests)
}

func TestOpenCodeResumeHighWaterScansEveryPage(t *testing.T) {
	recentByCreation := openCodeMessageIDFromEncoded(0x100000000001, "AAAAAAAAAAAAAA")
	olderFutureID := openCodeMessageIDFromEncoded(0x200000000001, "BBBBBBBBBBBBBB")
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests++
		require.Equal(t, "/session/sess-current/message", r.URL.Path)
		require.Equal(t, "100", r.URL.Query().Get("limit"))
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Query().Get("before") {
		case "":
			w.Header().Set("X-Next-Cursor", "page-2")
			require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
				"info": map[string]string{"id": recentByCreation, "role": "assistant"},
			}}))
		case "page-2":
			require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
				"info": map[string]string{"id": olderFutureID, "role": "assistant"},
			}}))
		default:
			t.Fatalf("unexpected cursor %q", r.URL.Query().Get("before"))
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL:  server.URL,
		password:   "pw",
		sessionID:  "sess-current",
		httpClient: server.Client(),
		workDir:    t.TempDir(),
	}
	require.NoError(t, driver.refreshOpenCodeResumedSessionMessageHighWater(context.Background(), "sess-current"))
	next, err := driver.nextRuntimeInputMessageID()
	require.NoError(t, err)
	require.Greater(t, next, olderFutureID)
	require.Equal(t, 2, requests)
}

func TestOpenCodeResumeHighWaterDoesNotApplyNoInitialCandidateValidation(t *testing.T) {
	userID := openCodeMessageIDFromEncoded(0x100000000003, "AAAAAAAAAAAAAA")
	assistantID := openCodeMessageIDFromEncoded(0x100000000004, "BBBBBBBBBBBBBB")
	differentParentID := openCodeMessageIDFromEncoded(0x100000000002, "CCCCCCCCCCCCCC")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{
			{"info": map[string]any{"id": userID, "role": "user"}},
			{"info": map[string]any{
				"id": assistantID, "role": "assistant", "parentID": differentParentID,
				"finish": "stop",
			}},
		}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	require.NoError(t, driver.refreshOpenCodeResumedSessionMessageHighWater(context.Background(), "sess-current"))
	next, err := driver.nextRuntimeInputMessageID()
	require.NoError(t, err)
	require.Greater(t, next, assistantID)
}

func TestOpenCodeResumeBootstrapUsesLexicographicLatestAcrossPages(t *testing.T) {
	newerByCreationTerminalID := openCodeMessageIDFromEncoded(0x100000000002, "AAAAAAAAAAAAAA")
	parentID := openCodeMessageIDFromEncoded(0x100000000001, "BBBBBBBBBBBBBB")
	olderByCreationUnansweredID := openCodeMessageIDFromEncoded(0x200000000001, "CCCCCCCCCCCCCC")
	var promptMessageID string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/session/sess-current/message":
			switch r.URL.Query().Get("before") {
			case "":
				w.Header().Set("X-Next-Cursor", "page-2")
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]any{
						"id":       newerByCreationTerminalID,
						"parentID": parentID,
						"role":     "assistant",
						"finish":   "stop",
						"time":     map[string]any{"completed": 2},
					},
				}}))
			case "page-2":
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]string{"id": olderByCreationUnansweredID, "role": "user"},
				}}))
			default:
				t.Fatalf("unexpected cursor %q", r.URL.Query().Get("before"))
			}
		case r.Method == http.MethodPost && r.URL.Path == "/session/sess-current/prompt_async":
			var body struct {
				MessageID string `json:"messageID"`
			}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			promptMessageID = body.MessageID
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	require.NoError(t, driver.recoverInterruptedResumedSession(context.Background(), "sess-current"))
	require.Equal(t, olderByCreationUnansweredID, promptMessageID)
}

func TestOpenCodeResumeBootstrapExcludesRowsBeforeCompletedCompactionTail(t *testing.T) {
	currentUserID := openCodeMessageIDFromEncoded(0x100000000005, "AAAAAAAAAAAAAA")
	summaryID := openCodeMessageIDFromEncoded(0x100000000004, "BBBBBBBBBBBBBB")
	compactionUserID := openCodeMessageIDFromEncoded(0x100000000003, "CCCCCCCCCCCCCC")
	tailStartID := openCodeMessageIDFromEncoded(0x100000000002, "DDDDDDDDDDDDDD")
	excludedFutureAssistantID := openCodeMessageIDFromEncoded(0x200000000001, "EEEEEEEEEEEEEE")
	excludedFutureParentID := openCodeMessageIDFromEncoded(0x200000000000, "FFFFFFFFFFFFFF")

	var promptMessageID string
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests++
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/session/sess-current/message":
			w.Header().Set("Content-Type", "application/json")
			switch r.URL.Query().Get("before") {
			case "":
				w.Header().Set("X-Next-Cursor", "page-2")
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]any{"id": currentUserID, "role": "user"},
				}}))
			case "page-2":
				w.Header().Set("X-Next-Cursor", "page-3")
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]any{
						"id": summaryID, "role": "assistant", "parentID": compactionUserID,
						"summary": true, "finish": "stop",
					},
				}}))
			case "page-3":
				w.Header().Set("X-Next-Cursor", "page-4")
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]any{"id": compactionUserID, "role": "user"},
					"parts": []map[string]any{{
						"type": "compaction", "tail_start_id": tailStartID,
					}},
				}}))
			case "page-4":
				w.Header().Set("X-Next-Cursor", "page-5")
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]any{"id": tailStartID, "role": "user"},
				}}))
			case "page-5":
				require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
					"info": map[string]any{
						"id": excludedFutureAssistantID, "role": "assistant",
						"parentID": excludedFutureParentID, "finish": "stop",
					},
				}}))
			default:
				t.Fatalf("unexpected cursor %q", r.URL.Query().Get("before"))
			}
		case r.Method == http.MethodPost && r.URL.Path == "/session/sess-current/prompt_async":
			var body struct {
				MessageID string `json:"messageID"`
			}
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			promptMessageID = body.MessageID
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
		}
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	require.NoError(t, driver.recoverInterruptedResumedSession(context.Background(), "sess-current"))
	require.Equal(t, currentUserID, promptMessageID)
	require.Equal(t, 6, requests)

	// High-water still considers every raw row, including rows excluded from
	// effective post-compaction history, so newly allocated ids cannot regress.
	next, err := driver.nextRuntimeInputMessageID()
	require.NoError(t, err)
	require.Greater(t, next, excludedFutureAssistantID)
}

func TestOpenCodeResumeHighWaterRejectsNonAscendingMessageID(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
			"info": map[string]string{"id": "msg_external", "role": "user"},
		}}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	err := driver.refreshOpenCodeResumedSessionMessageHighWater(context.Background(), "sess-current")
	require.Error(t, err)
	require.Contains(t, err.Error(), "unsupported non-ascending message id")
}

func TestOpenCodeResumeBootstrapRejectsEffectiveAssistantWithoutUser(t *testing.T) {
	assistantID := openCodeMessageIDFromEncoded(0x100000000002, "AAAAAAAAAAAAAA")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
			"info": map[string]any{
				"id": assistantID, "role": "assistant",
				"parentID": openCodeMessageIDFromEncoded(0x100000000001, "BBBBBBBBBBBBBB"),
			},
		}}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	err := driver.recoverInterruptedResumedSession(context.Background(), "sess-current")
	require.Error(t, err)
	require.Contains(t, err.Error(), "but no user message")
}

func TestOpenCodeResumePromptMessageIDClassifiesTail(t *testing.T) {
	tests := []struct {
		name, message, messageID, reason string
	}{
		{
			name: "unanswered user", message: `{"info":{"id":"msg_u","role":"user"}}`,
			messageID: "msg_u", reason: "unanswered_user",
		},
		{
			name:      "assistant row before model execution",
			message:   `{"info":{"id":"msg_a","role":"assistant","parentID":"msg_u","time":{"created":1}}}`,
			messageID: "msg_u", reason: "unfinished_assistant",
		},
		{
			name: "captured permission-blocked tool",
			message: `{
				"info":{"id":"msg_a","role":"assistant","parentID":"msg_u","time":{"created":1720000000000}},
				"parts":[{"id":"prt_tool","type":"tool","callID":"call_1","tool":"read",
					"state":{"status":"running","input":{"filePath":"/tmp/outside"}}}]
			}`,
			messageID: "msg_u", reason: "unfinished_assistant",
		},
		{
			name: "completed tool before next model step",
			message: `{"info":{"id":"msg_a","role":"assistant","parentID":"msg_u"},
				"parts":[{"type":"tool","state":{"status":"completed"}}]}`,
			messageID: "msg_u", reason: "unfinished_assistant",
		},
		{
			name: "finished assistant",
			message: `{"info":{"id":"msg_a","role":"assistant","parentID":"msg_u",
				"finish":"stop","time":{"created":1,"completed":2}}}`,
			reason: "terminal",
		},
		{
			name: "failed assistant",
			message: `{"info":{"id":"msg_a","role":"assistant","parentID":"msg_u",
				"error":{"name":"UnknownError"},"time":{"completed":2}}}`,
			reason: "terminal",
		},
		{
			name: "completed assistant without finish",
			message: `{"info":{"id":"msg_a","role":"assistant","parentID":"msg_u",
				"time":{"created":1,"completed":2}}}`,
			reason: "terminal",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var message openCodeResumeMessage
			require.NoError(t, json.Unmarshal([]byte(tt.message), &message))
			messageID, reason := openCodeResumePromptMessageID(message)
			require.Equal(t, tt.messageID, messageID)
			require.Equal(t, tt.reason, reason)
		})
	}
}

func TestOpenCodeRecoverInterruptedResumedSessionDoesNotPostTerminalTail(t *testing.T) {
	assistantID := openCodeMessageIDFromEncoded(0x100000000002, "AAAAAAAAAAAAAA")
	parentID := openCodeMessageIDFromEncoded(0x100000000001, "BBBBBBBBBBBBBB")
	requests := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests++
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/session/sess-current/message", r.URL.Path)
		require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{
			{
				"info": map[string]any{"id": parentID, "role": "user"},
			},
			{
				"info": map[string]any{
					"id": assistantID, "role": "assistant", "parentID": parentID,
					"finish": "stop", "time": map[string]any{"completed": 2},
				},
			},
		}))
	}))
	defer server.Close()

	driver := &OpenCodeDriver{
		serverURL: server.URL, password: "pw", sessionID: "sess-current",
		httpClient: server.Client(), workDir: t.TempDir(),
	}
	require.NoError(t, driver.recoverInterruptedResumedSession(context.Background(), "sess-current"))
	require.Equal(t, 1, requests)
}
