package agent

import (
	"context"
	"net/http"
	"strings"
	"testing"

	"github.com/ollama/ollama/api"
)

type scriptedCompactionClient struct {
	responses [][]api.ChatResponse
	errs      []error
	requests  []*api.ChatRequest
}

func (c *scriptedCompactionClient) Chat(_ context.Context, req *api.ChatRequest, fn api.ChatResponseFunc) error {
	c.requests = append(c.requests, req)
	i := len(c.requests) - 1
	if i < len(c.responses) {
		for _, response := range c.responses[i] {
			if err := fn(response); err != nil {
				return err
			}
		}
	}
	if i < len(c.errs) {
		return c.errs[i]
	}
	return nil
}

func assertCompactionSummaryPair(t *testing.T, messages []api.Message) {
	t.Helper()
	if len(messages) != 2 {
		t.Fatalf("compaction summary pair len = %d, want 2: %#v", len(messages), messages)
	}
	if messages[0].Role != "assistant" || len(messages[0].ToolCalls) != 1 || messages[0].ToolCalls[0].Function.Name != CompactionToolName {
		t.Fatalf("compaction assistant message = %#v", messages[0])
	}
	if messages[0].ToolCalls[0].Function.Arguments.Len() != 0 {
		t.Fatalf("compaction summary tool call should not have arguments: %#v", messages[0].ToolCalls[0].Function.Arguments.ToMap())
	}
	if messages[1].Role != "tool" || messages[1].ToolName != CompactionToolName || messages[1].ToolCallID != messages[0].ToolCalls[0].ID {
		t.Fatalf("compaction tool result = %#v", messages[1])
	}
	if !strings.HasPrefix(messages[1].Content, CompactionSummaryMessagePrefix) {
		t.Fatalf("compaction tool result missing summary prefix: %#v", messages[1])
	}
}

func TestSimpleCompactorSummarizesOldMessages(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 16000,
		KeepUserTurns:       2,
		Threshold:           0.5,
	}}

	messages := []api.Message{
		{Role: "system", Content: "stay pinned"},
		{Role: "user", Content: "old request"},
		{Role: "assistant", Content: "old answer", Thinking: "hidden"},
		{Role: "user", Content: "recent one"},
		{Role: "assistant", Content: "recent answer"},
		{Role: "user", Content: "recent two"},
	}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID:   "chat-1",
		Model:    "model",
		Messages: messages,
		Latest:   api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 12000}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	compacted := result.Messages
	if len(compacted) != 6 {
		t.Fatalf("compacted messages = %d, want 6", len(compacted))
	}
	if compacted[0].Content != "stay pinned" {
		t.Fatalf("first message = %#v", compacted[0])
	}
	if result.Summary != "summary" {
		t.Fatalf("result summary = %q", result.Summary)
	}
	assertCompactionSummaryPair(t, compacted[1:3])
	if compacted[3].Content != "recent one" || compacted[5].Content != "recent two" {
		t.Fatalf("recent turns were not kept: %#v", compacted)
	}
	if len(client.requests) != 1 {
		t.Fatalf("summary requests = %d, want 1", len(client.requests))
	}
	if strings.Contains(client.requests[0].Messages[1].Content, "hidden") {
		t.Fatal("compaction prompt should omit thinking")
	}
}

func TestSimpleCompactorKeepsOnlySummaryForSmallContext(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "small context summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: compactOnlySummaryContextTokens - 1,
		KeepUserTurns:       3,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID:       "chat-1",
		Model:        "model",
		ContinueTask: true,
		Messages: []api.Message{
			{Role: "system", Content: "pinned"},
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "latest request"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 12000}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	if len(result.Messages) != 3 {
		t.Fatalf("messages = %#v, want system plus compaction summary pair", result.Messages)
	}
	if result.Messages[0].Content != "pinned" {
		t.Fatalf("leading system message not kept: %#v", result.Messages)
	}
	assertCompactionSummaryPair(t, result.Messages[1:])
	if !strings.Contains(result.Messages[2].Content, CompactionContinueInstruction) {
		t.Fatalf("tool result missing continue instruction: %q", result.Messages[2].Content)
	}
}

func TestSimpleCompactorAddsContinueTaskInstructionOnlyToToolResult(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID:       "chat-1",
		Model:        "model",
		ContinueTask: true,
		Messages: []api.Message{
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent request"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 75}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if result.Summary != "summary" {
		t.Fatalf("result summary = %q", result.Summary)
	}
	content := result.Messages[1].Content
	if !strings.Contains(content, CompactionContinueInstruction) {
		t.Fatalf("tool result missing continue instruction: %q", content)
	}
	if got := CompactionSummaryText(content); got != "summary" {
		t.Fatalf("visible summary text = %q", got)
	}
}

func TestSimpleCompactorTruncatesOversizedSummary(t *testing.T) {
	longSummary := strings.Repeat("x", maxCompactionSummaryRunes+1024)
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: longSummary}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID: "chat-1",
		Model:  "model",
		Messages: []api.Message{
			{Role: "user", Content: "old one"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent one"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 75}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	if runeCount := len([]rune(result.Summary)); runeCount > maxCompactionSummaryRunes+200 {
		t.Fatalf("summary runes = %d, want <= %d (plus marker)", runeCount, maxCompactionSummaryRunes)
	}
	if !strings.Contains(result.Summary, "[summary truncated:") {
		t.Fatalf("summary missing truncation marker: %q", result.Summary)
	}
	if !strings.Contains(result.Messages[1].Content, "[summary truncated:") {
		t.Fatalf("compacted message missing truncation marker: %#v", result.Messages)
	}
}

func TestSimpleCompactorRetriesEmptySummaryWithThinkFalse(t *testing.T) {
	client := &scriptedCompactionClient{
		responses: [][]api.ChatResponse{
			{{Message: api.Message{Role: "assistant", Thinking: "internal summary plan"}}},
			{{Message: api.Message{Role: "assistant", Content: "fallback summary"}}},
		},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model: "model",
		Messages: []api.Message{
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent request"},
		},
		Force: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted || result.Summary != "fallback summary" {
		t.Fatalf("compaction result = %#v", result)
	}
	if len(client.requests) != 2 {
		t.Fatalf("summary requests = %d, want 2", len(client.requests))
	}
	if client.requests[0].Think != nil {
		t.Fatalf("first summary request think = %#v, want nil", client.requests[0].Think)
	}
	if client.requests[1].Think == nil || client.requests[1].Think.Value != false {
		t.Fatalf("fallback summary request think = %#v, want false", client.requests[1].Think)
	}
}

func TestSimpleCompactorIgnoresUnsupportedThinkFalseFallback(t *testing.T) {
	client := &scriptedCompactionClient{
		responses: [][]api.ChatResponse{
			{{Message: api.Message{Role: "assistant", Thinking: "internal summary plan"}}},
			nil,
		},
		errs: []error{
			nil,
			api.StatusError{StatusCode: http.StatusBadRequest, ErrorMessage: "model does not support thinking"},
		},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model: "model",
		Messages: []api.Message{
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent request"},
		},
		Force: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if result.Compacted || result.Reason != "summary was empty" {
		t.Fatalf("compaction result = %#v", result)
	}
	if len(client.requests) != 2 {
		t.Fatalf("summary requests = %d, want 2", len(client.requests))
	}
	if client.requests[1].Think == nil || client.requests[1].Think.Value != false {
		t.Fatalf("fallback summary request think = %#v, want false", client.requests[1].Think)
	}
}

func TestSimpleCompactorFallsBackToUnsetThinkWhenThinkFalseUnsupported(t *testing.T) {
	client := &scriptedCompactionClient{
		responses: [][]api.ChatResponse{
			{{Message: api.Message{Role: "assistant", Thinking: "internal summary plan"}}},
			nil,
			{{Message: api.Message{Role: "assistant", Content: "unset think summary"}}},
		},
		errs: []error{
			nil,
			api.StatusError{StatusCode: http.StatusBadRequest, ErrorMessage: "think level is not supported"},
			nil,
		},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}
	thinkHigh := &api.ThinkValue{Value: "high"}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model: "model",
		Messages: []api.Message{
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent request"},
		},
		Think: thinkHigh,
		Force: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted || result.Summary != "unset think summary" {
		t.Fatalf("compaction result = %#v", result)
	}
	if len(client.requests) != 3 {
		t.Fatalf("summary requests = %d, want 3", len(client.requests))
	}
	if client.requests[0].Think != thinkHigh {
		t.Fatalf("first summary request think = %#v, want original", client.requests[0].Think)
	}
	if client.requests[1].Think == nil || client.requests[1].Think.Value != false {
		t.Fatalf("fallback summary request think = %#v, want false", client.requests[1].Think)
	}
	if client.requests[2].Think != nil {
		t.Fatalf("unsupported fallback retry think = %#v, want nil", client.requests[2].Think)
	}
}

func TestSimpleCompactorKeepsFewerTurnsForShortChats(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "short summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 16000,
		KeepUserTurns:       3,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID: "chat-1",
		Model:  "model",
		Messages: []api.Message{
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "latest request"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 12000}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	if len(result.Messages) != 3 {
		t.Fatalf("messages = %#v, want compaction tool pair plus latest request", result.Messages)
	}
	assertCompactionSummaryPair(t, result.Messages[:2])
	if result.Messages[2].Content != "latest request" {
		t.Fatalf("latest turn was not kept: %#v", result.Messages)
	}
}

func TestSimpleCompactorCanArchiveWholeShortChat(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "whole summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       3,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID: "chat-1",
		Model:  "model",
		Messages: []api.Message{
			{Role: "user", Content: "only request"},
			{Role: "assistant", Content: "only answer"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 75}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	if len(result.Messages) != 2 {
		t.Fatalf("messages = %#v, want only compaction tool pair", result.Messages)
	}
	assertCompactionSummaryPair(t, result.Messages)
}

func TestSimpleCompactorSkipsBelowThreshold(t *testing.T) {
	client := &fakeClient{}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		Threshold:           0.8,
	}}

	messages := []api.Message{
		{Role: "user", Content: "one"},
		{Role: "user", Content: "two"},
		{Role: "user", Content: "three"},
		{Role: "user", Content: "four"},
		{Role: "user", Content: "five"},
	}
	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model:    "model",
		Messages: messages,
		Latest:   api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 50}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if result.Compacted {
		t.Fatal("did not expect compaction")
	}
	if result.Due {
		t.Fatal("below-threshold compaction should not be due")
	}
	if len(result.Messages) != len(messages) {
		t.Fatalf("messages changed below threshold: %#v", result.Messages)
	}
	if len(client.requests) != 0 {
		t.Fatalf("summary requests = %d, want 0", len(client.requests))
	}
}

func TestSimpleCompactorUsesEstimatedMessagesWhenPromptEvalMissing(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "estimated summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.8,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model: "model",
		Messages: []api.Message{
			{Role: "user", Content: "old request"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "read large output"},
			{Role: "assistant", ToolCalls: []api.ToolCall{{
				ID: "call-1",
				Function: api.ToolCallFunction{
					Name: "read",
				},
			}}},
			{Role: "tool", ToolName: "read", ToolCallID: "call-1", Content: strings.Repeat("x", 360)},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Due || !result.Compacted {
		t.Fatalf("expected estimate-driven compaction, got %#v", result)
	}
	if result.Summary != "estimated summary" {
		t.Fatalf("summary = %q", result.Summary)
	}
}

func TestSimpleCompactorEstimateIncludesRequestPreamble(t *testing.T) {
	compactor := &SimpleCompactor{Client: nil, Options: CompactionOptions{
		ContextWindowTokens: 100,
		Threshold:           0.8,
	}}

	if !compactor.shouldCompact(CompactionRequest{
		SystemPrompt: strings.Repeat("system ", 360),
		Messages:     []api.Message{{Role: "user", Content: "tiny"}},
	}) {
		t.Fatal("system prompt should count toward compaction estimate")
	}

	if !compactor.shouldCompact(CompactionRequest{
		Messages: []api.Message{{Role: "user", Content: "tiny"}},
		Tools: api.Tools{{
			Type: "function",
			Function: api.ToolFunction{
				Name:        "verbose_tool",
				Description: strings.Repeat("description ", 360),
			},
		}},
	}) {
		t.Fatal("tool definitions should count toward compaction estimate")
	}
}

func TestCompactionPromptFitsBudgetByTruncatingLargeToolOutput(t *testing.T) {
	largeToolOutput := strings.Repeat("x", 10_000)
	body, err := compactionPrompt("", []api.Message{
		{Role: "user", Content: "what changed?"},
		{Role: "assistant", ToolCalls: []api.ToolCall{{
			ID: "call-1",
			Function: api.ToolCallFunction{
				Name: "bash",
			},
		}}},
		{Role: "tool", ToolName: "bash", ToolCallID: "call-1", Content: largeToolOutput},
	}, 300)
	if err != nil {
		t.Fatal(err)
	}
	if estimateCompactionTokens(body) > 300 {
		t.Fatalf("compaction prompt tokens = %d, want <= 300", estimateCompactionTokens(body))
	}
	if strings.Count(body, "x") >= len(largeToolOutput) {
		t.Fatal("large tool output was not truncated")
	}
	if !strings.Contains(body, "[tool output truncated: showing first ~") {
		t.Fatalf("truncation marker missing from compaction prompt: %q", body)
	}
}

func TestCompactionPromptRetruncatesAlreadyTruncatedToolOutput(t *testing.T) {
	alreadyTruncated := strings.Repeat("x", 7000) + "\n\n[tool output truncated: showing first ~100 tokens and last ~100 tokens; omitted ~99999 tokens. Use a narrower command, line range, or search query if more detail is needed.]\n\n" + strings.Repeat("y", 7000)
	body, err := compactionPrompt("", []api.Message{
		{Role: "user", Content: "what changed?"},
		{Role: "assistant", ToolCalls: []api.ToolCall{{
			ID: "call-1",
			Function: api.ToolCallFunction{
				Name: "bash",
			},
		}}},
		{Role: "tool", ToolName: "bash", ToolCallID: "call-1", Content: alreadyTruncated},
	}, 300)
	if err != nil {
		t.Fatal(err)
	}
	if estimateCompactionTokens(body) > 300 {
		t.Fatalf("compaction prompt tokens = %d, want <= 300", estimateCompactionTokens(body))
	}
	if strings.Count(body, "x")+strings.Count(body, "y") >= 14_000 {
		t.Fatal("already-truncated tool output was not truncated again")
	}
	if !strings.Contains(body, "[tool output truncated: showing first ~") {
		t.Fatalf("truncation marker missing from compaction prompt: %q", body)
	}
}

func TestCompactionSummaryTextStripsPrefix(t *testing.T) {
	content := compactionSummaryMessageForTask("worked on branch changes", false)
	if got := CompactionSummaryText(content); got != "worked on branch changes" {
		t.Fatalf("summary text = %q", got)
	}
}

func TestCompactionSummaryCanTellModelToContinueTask(t *testing.T) {
	content := compactionSummaryMessageForTask("worked on branch changes", true)
	if !strings.Contains(content, CompactionContinueInstruction) {
		t.Fatalf("summary message missing continue instruction: %q", content)
	}
	if got := CompactionSummaryText(content); got != "worked on branch changes" {
		t.Fatalf("summary text = %q", got)
	}
}

func TestResolveContextWindowTokensPrefersExplicitNumCtx(t *testing.T) {
	tests := []struct {
		name       string
		options    map[string]any
		configured int
		want       int
	}{
		{
			name:       "explicit smaller num ctx",
			options:    map[string]any{"num_ctx": 4096},
			configured: 8192,
			want:       4096,
		},
		{
			name:       "explicit num ctx can exceed configured metadata",
			options:    map[string]any{"num_ctx": 131072},
			configured: 8192,
			want:       131072,
		},
		{
			name:       "metadata without explicit num ctx",
			configured: 32768,
			want:       32768,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ResolveContextWindowTokens(tt.options, tt.configured); got != tt.want {
				t.Fatalf("ResolveContextWindowTokens() = %d, want %d", got, tt.want)
			}
		})
	}
}

func TestSimpleCompactorForceCompactsWithoutPromptEvalCount(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "forced summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 100,
		KeepUserTurns:       1,
		Threshold:           0.8,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model: "model",
		Messages: []api.Message{
			{Role: "user", Content: "old"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent"},
		},
		Force: true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Due || !result.Compacted {
		t.Fatalf("forced compaction result = %#v", result)
	}
	if result.Summary != "forced summary" {
		t.Fatalf("summary = %q", result.Summary)
	}
}

func TestSimpleCompactorDefaultsToKeepingThreeUserTurns(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 16000,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		ChatID: "chat-1",
		Model:  "model",
		Messages: []api.Message{
			{Role: "user", Content: "old"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "one"},
			{Role: "assistant", Content: "one answer"},
			{Role: "user", Content: "two"},
			{Role: "assistant", Content: "two answer"},
			{Role: "user", Content: "three"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 12000}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	assertCompactionSummaryPair(t, result.Messages[:2])
	if got := result.Messages[2].Content; got != "one" {
		t.Fatalf("first kept turn = %q, want one", got)
	}
}

func TestSimpleCompactorCarriesPreviousSummary(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "new summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 16000,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}

	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model: "model",
		Messages: []api.Message{
			{Role: "system", Content: CompactionSummaryMessagePrefix + "old summary"},
			{Role: "user", Content: "old"},
			{Role: "assistant", Content: "old answer"},
			{Role: "user", Content: "recent"},
		},
		Latest: api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 12000}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	if !strings.Contains(client.requests[0].Messages[1].Content, "Previous summary:\nold summary") {
		t.Fatalf("previous summary missing from request: %q", client.requests[0].Messages[1].Content)
	}
}

func TestSimpleCompactorCarriesPreviousToolSummaryAndPlacesNewSummaryBeforeKeptSuffix(t *testing.T) {
	client := &fakeClient{
		responses: [][]api.ChatResponse{{
			{Message: api.Message{Role: "assistant", Content: "new summary"}},
		}},
	}
	compactor := &SimpleCompactor{Client: client, Options: CompactionOptions{
		ContextWindowTokens: 16000,
		KeepUserTurns:       1,
		Threshold:           0.5,
	}}

	messages := []api.Message{
		{Role: "user", Content: "kept before old summary"},
		CompactionSummaryMessages("old summary", false)[0],
		CompactionSummaryMessages("old summary", false)[1],
		{Role: "user", Content: "latest request"},
	}
	result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{
		Model:    "model",
		Messages: messages,
		Latest:   api.ChatResponse{Metrics: api.Metrics{PromptEvalCount: 12000}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Compacted {
		t.Fatal("expected compaction")
	}
	if !strings.Contains(client.requests[0].Messages[1].Content, "Previous summary:\nold summary") {
		t.Fatalf("previous summary missing from request: %q", client.requests[0].Messages[1].Content)
	}
	if len(result.Messages) != 3 {
		t.Fatalf("messages = %#v, want compaction pair plus latest request", result.Messages)
	}
	assertCompactionSummaryPair(t, result.Messages[:2])
	if result.Messages[2].Content != "latest request" {
		t.Fatalf("kept suffix = %#v", result.Messages)
	}
}
