//go:build unix

// Expired receipts stop recovery without losing the original intent. The real
// daemon admits each request before the proxy discards its acknowledgement.
package e2e

import (
	"errors"
	"fmt"
	"net/http"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"albedo/cli/internal/daemon"
	"albedo/cli/internal/tui"
	tea "charm.land/bubbletea/v2"
)

func operationRequests(proxy *acknowledgementProxy) []string {
	proxy.mu.Lock()
	defer proxy.mu.Unlock()
	return slices.DeleteFunc(slices.Clone(proxy.requests), func(request string) bool {
		receiptQuery := strings.HasPrefix(request, "GET /operations/")
		creation := request == "POST /sessions"
		submission := strings.HasPrefix(request, "POST /sessions/") && strings.HasSuffix(request, "/events")
		return !receiptQuery && !creation && !submission
	})
}

func TestTUIExpiredSubmissionsRemainUnresolvedAcrossNavigation(t *testing.T) {
	entered, release := make(chan struct{}), make(chan struct{})
	released := false
	defer func() {
		if !released {
			close(release)
		}
	}()
	var enteredOnce sync.Once
	profile := providerRoute(t, func(request map[string]any) string {
		if lastUserText(request) == "hold expired submissions" {
			enteredOnce.Do(func() { close(entered) })
			<-release
		}
		return echoReply(request)
	})
	session := daemonSession(t, newSession(t, t.TempDir()))
	other := daemonSession(t, newSession(t, t.TempDir()))
	if _, err := daemon.NewChatClient(conn(t), session.ID).Send(t.Context(), "hold expired submissions", nil); err != nil {
		t.Fatal(err)
	}
	select {
	case <-entered:
	case <-time.After(10 * time.Second):
		t.Fatal("active turn never started")
	}
	proxy := cutAcknowledgement(t, "/sessions/"+session.ID+"/events", expiredAcknowledgement)
	driver := driveTUIWithConnection(t, &session, proxy.connection)
	t.Cleanup(func() { driver.App.Chat.Close() })
	driver.Update(tea.WindowSizeMsg{Width: 120, Height: 100})
	driver.connected()
	var handles []*daemon.OperationHandle
	for index := 0; index < tui.MaxPendingUsers+1; index++ {
		prompt := fmt.Sprintf("unresolved user %d", index)
		driver.App.Chat.TextArea.SetValue(prompt)
		if index == 0 {
			driver.App.Chat.AttachedImage = &daemon.ImageAttachment{Data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC", ImageMetadata: daemon.ImageMetadata{MimeType: daemon.ImagePNG, Width: 1, Height: 1, Bytes: 69}}
		}
		var sent tui.ChatTurnSentMsg
		for _, message := range driver.results(driver.Update(tea.KeyPressMsg{Code: tea.KeyEnter})) {
			if result, ok := message.(tui.ChatTurnSentMsg); ok {
				sent = result
			}
			if cmd := driver.Update(message); cmd != nil {
				if _, ok := message.(tui.ChatTurnSentMsg); ok {
					t.Fatalf("expired submission scheduled recovery: %T", message)
				}
			}
		}
		uncertainty, ok := errors.AsType[*daemon.UncertainOutcomeError](sent.Err)
		if !ok || uncertainty.Handle != sent.Handle || !daemon.IsOperationExpired(sent.Err) || sent.Handle == nil {
			t.Fatalf("expired outcome lost original handle: %+v", sent)
		}
		handles = append(handles, sent.Handle)
	}
	driver.App.Chat.TextArea.SetValue(".")
	for _, message := range driver.results(driver.Update(tea.KeyPressMsg{Code: tea.KeyEnter})) {
		if sent, ok := message.(tui.ChatTurnSentMsg); ok {
			if sent.Handle == nil || !daemon.IsOperationExpired(sent.Err) {
				t.Fatalf("expired continuation: %+v", sent)
			}
			handles = append(handles, sent.Handle)
		}
		if cmd := driver.Update(message); cmd != nil {
			if _, ok := message.(tui.ChatTurnSentMsg); ok {
				t.Fatal("expired continuation scheduled recovery")
			}
		}
	}
	if len(handles) != tui.MaxPendingUsers+2 {
		t.Fatal("expired turns consumed pending capacity")
	}
	before := operationRequests(proxy)
	for _, handle := range handles {
		if cmd := driver.Update(tui.ChatOperationPollMsg{SessionID: session.ID, Generation: driver.App.Chat.Generation, Handle: handle}); cmd != nil {
			t.Fatal("expired operation resumed polling")
		}
	}
	driver.Update(tui.FolderOpenSessionMsg{Session: other})
	for _, message := range driver.results(driver.Update(tui.FolderOpenSessionMsg{Session: session})) {
		driver.Update(message)
	}
	driver.Update(tea.WindowSizeMsg{Width: 120, Height: 100})
	view := driver.View()
	for _, handle := range handles {
		if !strings.Contains(view, handle.ID()) {
			t.Fatalf("navigation lost unresolved identity %s:\n%s", handle.ID(), view)
		}
	}
	if after := operationRequests(proxy); !slices.Equal(before, after) {
		t.Fatalf("navigation resumed recovery: before=%v after=%v", before, after)
	}
	if len(before) != 2*len(handles) {
		t.Fatalf("expiry retried HTTP admission: %v", before)
	}
	// A durable event can still reconcile an outcome whose receipt has expired.
	close(release)
	released = true
	waitIdle(t, session.ID, profile, 2)
	history, err := daemon.NewChatClient(conn(t), session.ID).History(t.Context(), 0, 120)
	if err != nil {
		t.Fatal(err)
	}
	matched := false
	for _, event := range history.Events {
		if event.Type == daemon.EventUser && event.OperationID == handles[0].ID() {
			matched = true
			if event.Image == nil || event.Image.Width != 1 || event.Image.Height != 1 {
				t.Fatalf("expired image metadata lost: %+v", event)
			}
			driver.Update(tui.ChatStreamEventMsg{SessionID: session.ID, Generation: driver.App.Chat.Generation, Event: event})
		}
	}
	if !matched || strings.Contains(driver.View(), handles[0].ID()) {
		t.Fatal("durable user echo did not reconcile expired row")
	}

}

func TestTUICreationExpiryStopsRecoveryWithoutSwitchingSession(t *testing.T) {
	providerRoute(t, echoReply)
	initial := daemonSession(t, newSession(t, t.TempDir()))
	other := daemonSession(t, newSession(t, t.TempDir()))
	proxy := cutAcknowledgement(t, "/sessions", unresolvedAcknowledgement)
	driver := driveTUIWithConnection(t, &initial, proxy.connection)
	t.Cleanup(func() { driver.App.Chat.Close() })
	uncertain := driver.Send(tui.FolderNewSessionMsg{Workspace: t.TempDir()})
	driver.Update(tui.FolderOpenSessionMsg{Session: other})
	recovery := driver.Update(uncertain)
	if recovery == nil {
		t.Fatal("uncertain creation lost recovery")
	}
	proxy.allowReceipt.Store(true)
	proxy.expireReceipt.Store(true)
	var tick tea.Msg
	for _, message := range driver.results(recovery) {
		tick = message
		response := driver.Send(message)
		if cmd := driver.Update(response); cmd != nil {
			t.Fatal("expired creation scheduled further recovery")
		}
	}
	if driver.App.ActiveSession.ID != other.ID || !strings.Contains(driver.View(), "expired") {
		t.Fatalf("creation expiry switched session or lost notice:\n%s", driver.View())
	}
	before := operationRequests(proxy)
	operationID := strings.TrimPrefix(before[len(before)-1], "GET /operations/")
	if !strings.Contains(driver.View(), operationID) {
		t.Fatal("expired creation notice lost its operation identity")
	}
	if tick == nil || driver.Update(tick) != nil {
		t.Fatal("scheduled creation tick restarted expired lookup")
	}
	if after := operationRequests(proxy); !slices.Equal(before, after) {
		t.Fatal("creation expiry resumed HTTP recovery")
	}
}

func TestTUIRejectedSubmissionRestoresPromptAndImage(t *testing.T) {
	providerRoute(t, echoReply)
	driver := newTUIDriver(t)
	t.Cleanup(func() { driver.App.Chat.Close() })
	driver.connected()
	image := &daemon.ImageAttachment{Data: "invalid", ImageMetadata: daemon.ImageMetadata{MimeType: daemon.ImagePNG, Width: 1, Height: 1, Bytes: 1}}
	driver.App.Chat.AttachedImage = image
	driver.App.Chat.TextArea.SetValue("restore this rejected image")
	var sent tui.ChatTurnSentMsg
	for _, message := range driver.results(driver.Update(tea.KeyPressMsg{Code: tea.KeyEnter})) {
		if result, ok := message.(tui.ChatTurnSentMsg); ok {
			sent = result
		}
		driver.Update(message)
	}
	api, ok := errors.AsType[*daemon.APIError](sent.Err)
	if !ok || api.StatusCode != http.StatusBadRequest || sent.Handle == nil {
		t.Fatalf("real daemon did not reject image: %+v", sent)
	}
	if driver.App.Chat.TextArea.Value() != sent.Prompt || driver.App.Chat.AttachedImage != image {
		t.Fatal("rejection lost prompt or attachment")
	}
	// A following valid submission demonstrates that the rejected row left no pending intent.
	driver.App.Chat.AttachedImage = nil
	driver.App.Chat.TextArea.SetValue("valid after rejection")
	for _, message := range driver.results(driver.Update(tea.KeyPressMsg{Code: tea.KeyEnter})) {
		if result, ok := message.(tui.ChatTurnSentMsg); ok && result.Err != nil {
			t.Fatal(result.Err)
		}
	}
}
