// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: LicenseRef-MutuaL-1.2

package mcp

import (
	"context"
	"encoding/json"
	"slices"
	"strings"
	"testing"

	sdk "github.com/modelcontextprotocol/go-sdk/mcp"

	"git.secluded.site/cooked-mcp/internal/cooked"
)

func TestCallToolACIDToolsSurface1ReadsShoppingList(t *testing.T) {
	backend := &fakeBackend{shoppingList: cooked.ShoppingList{
		Aisles: []cooked.Aisle{{
			ID:   "pantry",
			Name: "Pantry",
			ProductGroups: []cooked.ProductGroup{{
				ID:       "pasta",
				Name:     "Pasta",
				Quantity: "200g",
			}},
		}},
	}}
	server := NewServer(backend, "test")

	output, err := server.CallReadTool(context.Background(), ReadArguments{Target: "shopping_list"})
	if err != nil {
		t.Fatalf("CallReadTool() error = %v", err)
	}

	if len(output.Aisles) != 1 {
		t.Fatalf("structured aisles = %d, want 1", len(output.Aisles))
	}
	if output.Aisles[0].ProductGroups[0].Name != "Pasta" {
		t.Fatalf("first item = %q, want Pasta", output.Aisles[0].ProductGroups[0].Name)
	}
}

func TestCallToolACIDRecipesRead1ListsRecipes(t *testing.T) {
	backend := &fakeBackend{
		recipes: []cooked.RecipeCard{
			{ID: testRecipeID, Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"},
		},
	}
	server := NewServer(backend, "test")

	output, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipes", Page: 2, Limit: 5})
	if err != nil {
		t.Fatalf("CallReadTool() error = %v", err)
	}

	if backend.page != 2 {
		t.Fatalf("backend page = %d, want 2", backend.page)
	}
	if backend.limit != 5 {
		t.Fatalf("backend limit = %d, want 5", backend.limit)
	}
	if len(output.Recipes) != 1 {
		t.Fatalf("structured recipes = %d, want 1", len(output.Recipes))
	}
	if output.Recipes[0].ID != testRecipeID || output.Recipes[0].Title != "Pasta" {
		t.Fatalf("first recipe = %#v, want test recipe ID Pasta", output.Recipes[0])
	}
}

func TestCallToolACIDRecipesPagination4DefaultsAndCapsRecipeLimit(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipes", Limit: 99})
	if err != nil {
		t.Fatalf("CallReadTool() error = %v", err)
	}

	if backend.page != 1 {
		t.Fatalf("backend page = %d, want default page 1", backend.page)
	}
	if backend.limit != 30 {
		t.Fatalf("backend limit = %d, want capped limit 30", backend.limit)
	}
}

func TestCallToolACIDRecipesRead2SearchesRecipes(t *testing.T) {
	backend := &fakeBackend{searchRecipes: []cooked.RecipeCard{
		{ID: testRecipeID, Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"},
		{ID: testOtherRecipeID, Title: "Tomato Pasta", ThumbnailURL: "https://example.invalid/thumb2.jpg"},
	}}
	server := NewServer(backend, "test")

	output, err := server.CallReadTool(context.Background(), ReadArguments{
		Target: "recipes",
		Query:  " pasta ",
		Page:   2,
		Limit:  1,
	})
	if err != nil {
		t.Fatalf("CallReadTool() error = %v", err)
	}

	if backend.searchQuery != "pasta" {
		t.Fatalf("backend search query = %q, want pasta", backend.searchQuery)
	}
	if backend.searchPage != 2 {
		t.Fatalf("backend search page = %d, want 2", backend.searchPage)
	}
	if len(output.Recipes) != 1 {
		t.Fatalf("structured recipes = %d, want truncated result length 1", len(output.Recipes))
	}
	if output.Recipes[0].ID != testRecipeID || output.Recipes[0].Title != "Pasta" {
		t.Fatalf("first recipe = %#v, want test recipe ID Pasta", output.Recipes[0])
	}
}

// recipes.READ.5 recipes.READ.8-1
func TestCallToolACIDRecipesRead5And8_1ReadsSingleRecipe(t *testing.T) {
	backend := &fakeBackend{
		recipeMetadata: cooked.RecipeMetadata{
			Title:          "Pasta",
			ImageURLs:      []string{"https://example.invalid/pasta.jpg"},
			Owner:          "returned-user",
			EditPermission: true,
		},
		recipeContent: cooked.RecipeContent{
			Content:  "# Pasta\n\n- 200g pasta\n\n1. Boil pasta.",
			Portions: 1.5,
		},
	}
	server := NewServer(backend, "test")

	result, output, err := server.callReadTool(
		context.Background(),
		nil,
		ReadArguments{Target: "recipe", RecipeID: testRecipeID},
	)
	if err != nil {
		t.Fatalf("callReadTool() error = %v", err)
	}

	requireSingleRecipeBackendCalls(t, backend)
	requireSingleRecipeOutput(t, output)
	requireStructuredContent(t, result, output)
	requireNoImageLeak(t, result, output)
}

func requireSingleRecipeBackendCalls(t *testing.T, backend *fakeBackend) {
	t.Helper()

	if backend.metadataRecipeID != testRecipeID || backend.contentRecipeID != testRecipeID {
		t.Fatalf(
			"backend recipe IDs = metadata %q content %q, want test recipe ID",
			backend.metadataRecipeID,
			backend.contentRecipeID,
		)
	}
	if backend.metadataCalls != 1 || backend.contentCalls != 1 {
		t.Fatalf("backend calls = metadata %d content %d, want 1/1", backend.metadataCalls, backend.contentCalls)
	}
}

func requireSingleRecipeOutput(t *testing.T, output ReadOutput) {
	t.Helper()

	if output.Recipe == nil {
		t.Fatal("structured recipe is nil")
	}
	if output.Recipe.ID != testRecipeID || output.Recipe.Title != "Pasta" {
		t.Fatalf("structured recipe identity = %#v, want test recipe ID Pasta", output.Recipe)
	}
	if output.Recipe.Owner != "returned-user" {
		t.Fatalf("structured recipe owner = %q, want returned-user", output.Recipe.Owner)
	}
	if output.Recipe.Content != "# Pasta\n\n- 200g pasta\n\n1. Boil pasta." || output.Recipe.Portions != 1.5 {
		t.Fatalf(
			"structured recipe content/portions = %q/%g, want markdown/1.5",
			output.Recipe.Content,
			output.Recipe.Portions,
		)
	}
}

func requireNoImageLeak(t *testing.T, result *sdk.CallToolResult, output ReadOutput) {
	t.Helper()

	encodedOutput, err := json.Marshal(output)
	if err != nil {
		t.Fatalf("marshal output: %v", err)
	}
	if strings.Contains(string(encodedOutput), "image") ||
		strings.Contains(string(encodedOutput), "https://example.invalid/pasta.jpg") {
		t.Fatalf("structured output leaked image data: %s", encodedOutput)
	}
	text := requireTextContent(t, result)
	if strings.Contains(text, "https://example.invalid/pasta.jpg") {
		t.Fatalf("text output leaked image URL: %s", text)
	}
}

func TestCallToolACIDToolsReadTool3RequiresRecipeIDForRecipeTarget(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipe", RecipeID: "   "})
	if err == nil {
		t.Fatal("CallReadTool() error = nil, want missing recipe_id error")
	}

	if backend.metadataCalls != 0 || backend.contentCalls != 0 {
		t.Fatalf("backend calls = metadata %d content %d, want none", backend.metadataCalls, backend.contentCalls)
	}
}

// recipes.PREVIEW_TEXT.1 recipes.PREVIEW_TEXT.4-1
func TestCallToolACIDRecipesPreviewText1And4_1PreviewsRawText(t *testing.T) {
	backend := &fakeBackend{preview: cooked.RecipeTextPreview{
		Title:    "Pasta with Tomato Sauce",
		Markdown: "# Pasta\n\n1. Boil pasta.",
		Portions: 2.5,
	}}
	server := NewServer(backend, "test")

	result, output, err := server.callExtractIntoRecipeTool(
		context.Background(),
		nil,
		ExtractIntoRecipeArguments{Title: " Pasta ", Text: "  Boil pasta.\n"},
	)
	if err != nil {
		t.Fatalf("callExtractIntoRecipeTool() error = %v", err)
	}

	if backend.previewCalls != 1 {
		t.Fatalf("preview calls = %d, want 1", backend.previewCalls)
	}
	if backend.previewTitle != "Pasta" {
		t.Fatalf("backend preview title = %q, want trimmed Pasta", backend.previewTitle)
	}
	if backend.previewText != "  Boil pasta.\n" {
		t.Fatalf("backend preview text = %q, want raw text preserved", backend.previewText)
	}
	if output.Title != "Pasta with Tomato Sauce" || output.Markdown != "# Pasta\n\n1. Boil pasta." ||
		output.Portions != 2.5 {
		t.Fatalf("extraction output = %#v, want title/markdown/portions", output)
	}
	requireStructuredContent(t, result, output)
}

func TestCallToolACIDToolsExtractIntoRecipeTool1RequiresTitle(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callExtractIntoRecipeTool(
		context.Background(),
		nil,
		ExtractIntoRecipeArguments{Title: "   ", Text: "Boil pasta."},
	)
	if err == nil {
		t.Fatal("callExtractIntoRecipeTool() error = nil, want missing title error")
	}
	if backend.previewCalls != 0 {
		t.Fatalf("preview calls = %d, want none", backend.previewCalls)
	}
}

func TestCallToolACIDToolsExtractIntoRecipeTool2RequiresText(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callExtractIntoRecipeTool(
		context.Background(),
		nil,
		ExtractIntoRecipeArguments{Title: "Pasta", Text: "   "},
	)
	if err == nil {
		t.Fatal("callExtractIntoRecipeTool() error = nil, want missing text error")
	}
	if backend.previewCalls != 0 {
		t.Fatalf("preview calls = %d, want none", backend.previewCalls)
	}
}

// recipes.SAVE.2 recipes.SAVE.8-4 recipes.SAVE.9
func TestCallToolACIDRecipesSave2And8_4And9SavesPreparedRecipe(t *testing.T) {
	backend := &fakeBackend{saveRecipeID: testRecipeID}
	server := NewServer(backend, "test")

	result, output, err := server.callSaveRecipeTool(
		context.Background(),
		nil,
		SaveRecipeArguments{
			Source:   " prepared ",
			Title:    " Pasta ",
			Markdown: "  # Pasta\n\n1. Boil pasta.\n",
			Portions: 2.5,
		},
	)
	if err != nil {
		t.Fatalf("callSaveRecipeTool() error = %v", err)
	}

	if backend.saveCalls != 1 {
		t.Fatalf("save calls = %d, want 1", backend.saveCalls)
	}
	if backend.saveTitle != "Pasta" {
		t.Fatalf("backend save title = %q, want trimmed Pasta", backend.saveTitle)
	}
	if backend.saveMarkdown != "  # Pasta\n\n1. Boil pasta.\n" {
		t.Fatalf("backend save markdown = %q, want raw markdown preserved", backend.saveMarkdown)
	}
	if backend.savePortions != 2.5 {
		t.Fatalf("backend save portions = %g, want 2.5", backend.savePortions)
	}
	if output.RecipeID != testRecipeID {
		t.Fatalf("save output recipe ID = %q, want test recipe ID", output.RecipeID)
	}
	requireStructuredContent(t, result, output)
}

func TestCallToolACIDRecipesSave2_1To2_2RequiresPreparedTitleAndMarkdown(t *testing.T) {
	tests := []struct {
		name      string
		arguments SaveRecipeArguments
	}{
		{
			name:      "title",
			arguments: SaveRecipeArguments{Source: "prepared", Title: "   ", Markdown: "# Pasta", Portions: 2},
		},
		{
			name:      "markdown",
			arguments: SaveRecipeArguments{Source: "prepared", Title: "Pasta", Markdown: "   ", Portions: 2},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callSaveRecipeTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callSaveRecipeTool() error = nil, want missing prepared field error")
			}
			if backend.saveCalls != 0 {
				t.Fatalf("save calls = %d, want none", backend.saveCalls)
			}
		})
	}
}

func TestCallToolACIDRecipesSave1And11SavesRawTextRecipe(t *testing.T) {
	backend := &fakeBackend{
		preview: cooked.RecipeTextPreview{
			Title:    "Pasta with Tomato Sauce",
			Markdown: "# Pasta\n\n1. Boil pasta.",
			Portions: 2,
		},
		saveRecipeID: testRecipeID,
	}
	server := NewServer(backend, "test")

	_, output, err := server.callSaveRecipeTool(
		context.Background(),
		nil,
		SaveRecipeArguments{Source: " raw_text ", Title: " Pasta ", Text: "  Boil pasta.\n"},
	)
	if err != nil {
		t.Fatalf("callSaveRecipeTool() error = %v", err)
	}

	if backend.previewCalls != 1 || backend.saveCalls != 1 {
		t.Fatalf("backend calls = preview %d save %d, want 1/1", backend.previewCalls, backend.saveCalls)
	}
	if backend.previewTitle != "Pasta" {
		t.Fatalf("preview title = %q, want trimmed Pasta", backend.previewTitle)
	}
	if backend.previewText != "  Boil pasta.\n" {
		t.Fatalf("preview text = %q, want raw text preserved", backend.previewText)
	}
	if backend.saveTitle != "Pasta with Tomato Sauce" {
		t.Fatalf("save title = %q, want preview title", backend.saveTitle)
	}
	if backend.saveMarkdown != "# Pasta\n\n1. Boil pasta." {
		t.Fatalf("save markdown = %q, want preview markdown", backend.saveMarkdown)
	}
	if backend.savePortions != 2 {
		t.Fatalf("save portions = %g, want preview portions 2", backend.savePortions)
	}
	if output.RecipeID != testRecipeID {
		t.Fatalf("save output recipe ID = %q, want test recipe ID", output.RecipeID)
	}
}

func TestCallToolACIDRecipesSave1_1To1_2RequiresRawTextFields(t *testing.T) {
	tests := []struct {
		name      string
		arguments SaveRecipeArguments
	}{
		{name: "title", arguments: SaveRecipeArguments{Source: "raw_text", Title: "   ", Text: "Boil pasta."}},
		{name: "text", arguments: SaveRecipeArguments{Source: "raw_text", Title: "Pasta", Text: "   "}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callSaveRecipeTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callSaveRecipeTool() error = nil, want missing raw text field error")
			}
			if backend.previewCalls != 0 || backend.saveCalls != 0 {
				t.Fatalf("backend calls = preview %d save %d, want none", backend.previewCalls, backend.saveCalls)
			}
		})
	}
}

// recipes.SAVE.8 recipes.SAVE.8-4 recipes.SAVE.9
func TestCallToolACIDRecipesSave8And8_4And9UpdatesExistingRecipe(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, output, err := server.callSaveRecipeTool(
		context.Background(),
		nil,
		SaveRecipeArguments{
			Source:   " existing ",
			RecipeID: " " + testRecipeID + " ",
			Markdown: "  # Pasta\n\n1. Boil pasta.\n",
			Portions: 4.5,
		},
	)
	if err != nil {
		t.Fatalf("callSaveRecipeTool() error = %v", err)
	}

	if backend.updateCalls != 1 {
		t.Fatalf("update calls = %d, want 1", backend.updateCalls)
	}
	if backend.updateRecipeID != testRecipeID {
		t.Fatalf("backend update recipe ID = %q, want test recipe ID", backend.updateRecipeID)
	}
	if backend.updateMarkdown != "  # Pasta\n\n1. Boil pasta.\n" {
		t.Fatalf("backend update markdown = %q, want raw markdown preserved", backend.updateMarkdown)
	}
	if backend.updatePortions != 4.5 {
		t.Fatalf("backend update portions = %g, want 4.5", backend.updatePortions)
	}
	if output.RecipeID != testRecipeID {
		t.Fatalf("save output recipe ID = %q, want test recipe ID", output.RecipeID)
	}
}

func TestCallToolACIDRecipesSave8_1To8_2RequiresExistingRecipeIDAndMarkdown(t *testing.T) {
	tests := []struct {
		name      string
		arguments SaveRecipeArguments
	}{
		{
			name:      "recipe ID",
			arguments: SaveRecipeArguments{Source: "existing", RecipeID: "   ", Markdown: "# Pasta", Portions: 4},
		},
		{
			name:      "markdown",
			arguments: SaveRecipeArguments{Source: "existing", RecipeID: testRecipeID, Markdown: "   ", Portions: 4},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callSaveRecipeTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callSaveRecipeTool() error = nil, want missing existing field error")
			}
			if backend.updateCalls != 0 {
				t.Fatalf("update calls = %d, want none", backend.updateCalls)
			}
		})
	}
}

func TestCallToolACIDRecipesDelete1And2DeletesRecipe(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	result, output, err := server.callDeleteRecipeTool(
		context.Background(),
		nil,
		DeleteRecipeArguments{RecipeID: " " + testRecipeID + " "},
	)
	if err != nil {
		t.Fatalf("callDeleteRecipeTool() error = %v", err)
	}

	if backend.deleteCalls != 1 {
		t.Fatalf("delete calls = %d, want 1", backend.deleteCalls)
	}
	if backend.deleteRecipeID != testRecipeID {
		t.Fatalf("backend delete recipe ID = %q, want test recipe ID", backend.deleteRecipeID)
	}
	if output.RecipeID != testRecipeID {
		t.Fatalf("delete output recipe ID = %q, want test recipe ID", output.RecipeID)
	}
	requireStructuredContent(t, result, output)
}

func TestCallToolACIDToolsDeleteRecipeTool1RequiresRecipeID(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callDeleteRecipeTool(context.Background(), nil, DeleteRecipeArguments{RecipeID: "   "})
	if err == nil {
		t.Fatal("callDeleteRecipeTool() error = nil, want missing recipe_id error")
	}
	if backend.deleteCalls != 0 {
		t.Fatalf("delete calls = %d, want none", backend.deleteCalls)
	}
}

func TestDeleteRecipeToolACIDToolsAnnotations2To5MarksDestructiveOpenWorldTool(t *testing.T) {
	tool := deleteRecipeTool()
	if tool.Name != deleteRecipeToolName {
		t.Fatalf("tool name = %q, want %q", tool.Name, deleteRecipeToolName)
	}
	if tool.Annotations == nil {
		t.Fatal("tool annotations nil")
	}
	if tool.Annotations.ReadOnlyHint {
		t.Fatal("delete_recipe read-only hint = true, want false")
	}
	if tool.Annotations.DestructiveHint == nil || !*tool.Annotations.DestructiveHint {
		t.Fatalf("delete_recipe destructive hint = %#v, want true", tool.Annotations.DestructiveHint)
	}
	if tool.Annotations.OpenWorldHint == nil || !*tool.Annotations.OpenWorldHint {
		t.Fatalf("delete_recipe open-world hint = %#v, want true", tool.Annotations.OpenWorldHint)
	}
	if tool.Annotations.Title == "" {
		t.Fatal("delete_recipe annotation title empty")
	}
}

func TestCallToolACIDShoppingListClear1And2ClearsShoppingList(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	result, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{Action: " clear "},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}

	if backend.clearShoppingListCalls != 1 {
		t.Fatalf("clear calls = %d, want 1", backend.clearShoppingListCalls)
	}
	if !output.Cleared {
		t.Fatalf("clear output cleared = %v, want true", output.Cleared)
	}
	requireStructuredContent(t, result, output)
}

func TestCallToolACIDShoppingListActions2RejectsUnknownActionBeforeCallingCooked(t *testing.T) {
	tests := []struct {
		name      string
		arguments ChangeShoppingListArguments
		wantError string
	}{
		{name: "empty", arguments: ChangeShoppingListArguments{Action: "   "}, wantError: "action is required"},
		{
			name:      "unknown",
			arguments: ChangeShoppingListArguments{Action: "dance"},
			wantError: `unsupported change_shopping_list action "dance" (supported: add, update_item, replace_selection, add_selection, remove_selection, remove, clear)`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callChangeShoppingListTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callChangeShoppingListTool() error = nil, want action error")
			}
			if err.Error() != tt.wantError {
				t.Fatalf("callChangeShoppingListTool() error = %q", err.Error())
			}
			if backend.clearShoppingListCalls != 0 {
				t.Fatalf("clear calls = %d, want none", backend.clearShoppingListCalls)
			}
		})
	}
}

func TestCallToolACIDShoppingListAdd1To4AddsIngredients(t *testing.T) {
	backend := &fakeBackend{addShoppingListResult: cooked.AddShoppingListResult{
		AddedCount:  2,
		Ingredients: []string{"200g pasta", "1 cup tomato sauce"},
	}}
	server := NewServer(backend, "test")

	_, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{
			Action:      " add ",
			Ingredients: "200g pasta\n1 cup tomato sauce",
			RecipeID:    " draft-1 ",
		},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}

	if backend.addShoppingListCalls != 1 {
		t.Fatalf("add calls = %d, want 1", backend.addShoppingListCalls)
	}
	if backend.addIngredients != "200g pasta\n1 cup tomato sauce" {
		t.Fatalf("backend ingredients = %q, want raw multiline ingredients", backend.addIngredients)
	}
	if backend.addRecipeID != "draft-1" {
		t.Fatalf("backend recipe ID = %q, want trimmed opaque draft ID", backend.addRecipeID)
	}
	if output.AddedCount != 2 {
		t.Fatalf("output added count = %d, want 2", output.AddedCount)
	}
	if len(output.Ingredients) != 2 || output.Ingredients[0] != "200g pasta" {
		t.Fatalf("output ingredients = %#v, want added ingredients", output.Ingredients)
	}
}

func TestCallToolACIDToolsChangeShoppingListTool2RequiresIngredients(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{Action: "add", Ingredients: "   "},
	)
	if err == nil {
		t.Fatal("callChangeShoppingListTool() error = nil, want missing ingredients error")
	}
	if backend.addShoppingListCalls != 0 {
		t.Fatalf("add calls = %d, want none", backend.addShoppingListCalls)
	}
}

func TestCallToolACIDShoppingListRemove1And2RemovesProductGroups(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{
			Action:          " remove ",
			ProductGroupIDs: []string{" " + testProductGroupID + " ", testProductGroupID2, "   "},
		},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}

	if backend.removeShoppingListCalls != 1 {
		t.Fatalf("remove calls = %d, want 1", backend.removeShoppingListCalls)
	}
	if len(backend.removeProductGroupIDs) != 2 || backend.removeProductGroupIDs[0] != testProductGroupID ||
		backend.removeProductGroupIDs[1] != testProductGroupID2 {
		t.Fatalf("backend remove IDs = %#v, want valid test product group IDs", backend.removeProductGroupIDs)
	}
	if len(output.RemovedProductGroupIDs) != 2 || output.RemovedProductGroupIDs[0] != testProductGroupID ||
		output.RemovedProductGroupIDs[1] != testProductGroupID2 {
		t.Fatalf("output removed IDs = %#v, want valid test product group IDs", output.RemovedProductGroupIDs)
	}
}

func TestCallToolACIDShoppingListSelection1And5And6And8ReplacesSelectedSet(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{
			Action:          " replace_selection ",
			ProductGroupIDs: []string{" " + testProductGroupID + " ", testProductGroupID2, "   "},
		},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}

	if backend.replaceSelectionCalls != 1 {
		t.Fatalf("replace selection calls = %d, want 1", backend.replaceSelectionCalls)
	}
	if len(backend.replaceSelectionProductGroupIDs) != 2 ||
		backend.replaceSelectionProductGroupIDs[0] != testProductGroupID ||
		backend.replaceSelectionProductGroupIDs[1] != testProductGroupID2 {
		t.Fatalf(
			"backend selected IDs = %#v, want valid test product group IDs",
			backend.replaceSelectionProductGroupIDs,
		)
	}
	if len(output.SelectedProductGroupIDs) != 2 || output.SelectedProductGroupIDs[0] != testProductGroupID ||
		output.SelectedProductGroupIDs[1] != testProductGroupID2 {
		t.Fatalf("output selected IDs = %#v, want valid test product group IDs", output.SelectedProductGroupIDs)
	}
}

func TestCallToolACIDShoppingListSelection7RequiresReplaceSelectionProductGroupIDs(t *testing.T) {
	tests := []struct {
		name      string
		arguments ChangeShoppingListArguments
	}{
		{name: "nil", arguments: ChangeShoppingListArguments{Action: "replace_selection"}},
		{
			name:      "blank",
			arguments: ChangeShoppingListArguments{Action: "replace_selection", ProductGroupIDs: []string{"  "}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callChangeShoppingListTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callChangeShoppingListTool() error = nil, want missing product_group_ids error")
			}
			if backend.replaceSelectionCalls != 0 {
				t.Fatalf("replace selection calls = %d, want none", backend.replaceSelectionCalls)
			}
		})
	}
}

func TestCallToolACIDShoppingListSelection2And4And5And6And9AddsSelectedSet(t *testing.T) {
	backend := &fakeBackend{shoppingList: cooked.ShoppingList{Aisles: []cooked.Aisle{{
		ProductGroups: []cooked.ProductGroup{
			{ID: testProductGroupID, Selected: true},
			{ID: testProductGroupID2},
			{ID: testProductGroupID3, Selected: true},
		},
	}}}}
	server := NewServer(backend, "test")

	_, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{
			Action:          " add_selection ",
			ProductGroupIDs: []string{" " + testProductGroupID2 + " ", testProductGroupID, "   ", testProductGroupID4},
		},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}

	if backend.readShoppingListCalls != 1 {
		t.Fatalf("read shopping-list calls = %d, want 1", backend.readShoppingListCalls)
	}
	if backend.replaceSelectionCalls != 1 {
		t.Fatalf("replace selection calls = %d, want 1", backend.replaceSelectionCalls)
	}
	expectedIDs := []string{testProductGroupID, testProductGroupID3, testProductGroupID2, testProductGroupID4}
	if !slices.Equal(backend.replaceSelectionProductGroupIDs, expectedIDs) {
		t.Fatalf("backend selected IDs = %#v, want %#v", backend.replaceSelectionProductGroupIDs, expectedIDs)
	}
	if !slices.Equal(output.SelectedProductGroupIDs, expectedIDs) {
		t.Fatalf("output selected IDs = %#v, want %#v", output.SelectedProductGroupIDs, expectedIDs)
	}
}

func TestCallToolACIDShoppingListSelection7RequiresAddSelectionProductGroupIDs(t *testing.T) {
	tests := []struct {
		name      string
		arguments ChangeShoppingListArguments
	}{
		{name: "nil", arguments: ChangeShoppingListArguments{Action: "add_selection"}},
		{
			name:      "blank",
			arguments: ChangeShoppingListArguments{Action: "add_selection", ProductGroupIDs: []string{"  "}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callChangeShoppingListTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callChangeShoppingListTool() error = nil, want missing product_group_ids error")
			}
			if backend.readShoppingListCalls != 0 {
				t.Fatalf("read shopping-list calls = %d, want none", backend.readShoppingListCalls)
			}
			if backend.replaceSelectionCalls != 0 {
				t.Fatalf("replace selection calls = %d, want none", backend.replaceSelectionCalls)
			}
		})
	}
}

func TestCallToolACIDShoppingListSelection3And4And5And6And10RemovesSelectedSet(t *testing.T) {
	backend := &fakeBackend{shoppingList: cooked.ShoppingList{Aisles: []cooked.Aisle{{
		ProductGroups: []cooked.ProductGroup{
			{ID: testProductGroupID, Selected: true},
			{ID: testProductGroupID2, Selected: true},
			{ID: testProductGroupID3, Selected: true},
			{ID: testProductGroupID4},
		},
	}}}}
	server := NewServer(backend, "test")

	_, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{
			Action:          " remove_selection ",
			ProductGroupIDs: []string{" " + testProductGroupID2 + " ", testProductGroupID4, "   "},
		},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}

	if backend.readShoppingListCalls != 1 {
		t.Fatalf("read shopping-list calls = %d, want 1", backend.readShoppingListCalls)
	}
	if backend.replaceSelectionCalls != 1 {
		t.Fatalf("replace selection calls = %d, want 1", backend.replaceSelectionCalls)
	}
	expectedIDs := []string{testProductGroupID, testProductGroupID3}
	if !slices.Equal(backend.replaceSelectionProductGroupIDs, expectedIDs) {
		t.Fatalf("backend selected IDs = %#v, want %#v", backend.replaceSelectionProductGroupIDs, expectedIDs)
	}
	if !slices.Equal(output.SelectedProductGroupIDs, expectedIDs) {
		t.Fatalf("output selected IDs = %#v, want %#v", output.SelectedProductGroupIDs, expectedIDs)
	}
}

func TestCallToolACIDShoppingListSelection7RequiresRemoveSelectionProductGroupIDs(t *testing.T) {
	tests := []struct {
		name      string
		arguments ChangeShoppingListArguments
	}{
		{name: "nil", arguments: ChangeShoppingListArguments{Action: "remove_selection"}},
		{
			name:      "blank",
			arguments: ChangeShoppingListArguments{Action: "remove_selection", ProductGroupIDs: []string{"  "}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callChangeShoppingListTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callChangeShoppingListTool() error = nil, want missing product_group_ids error")
			}
			if backend.readShoppingListCalls != 0 {
				t.Fatalf("read shopping-list calls = %d, want none", backend.readShoppingListCalls)
			}
			if backend.replaceSelectionCalls != 0 {
				t.Fatalf("replace selection calls = %d, want none", backend.replaceSelectionCalls)
			}
		})
	}
}

func TestCallToolACIDShoppingListSafety1RequiresRemoveProductGroupIDs(t *testing.T) {
	tests := []struct {
		name      string
		arguments ChangeShoppingListArguments
	}{
		{name: "nil", arguments: ChangeShoppingListArguments{Action: "remove"}},
		{name: "blank", arguments: ChangeShoppingListArguments{Action: "remove", ProductGroupIDs: []string{"  "}}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			backend := &fakeBackend{}
			server := NewServer(backend, "test")

			_, _, err := server.callChangeShoppingListTool(context.Background(), nil, tt.arguments)
			if err == nil {
				t.Fatal("callChangeShoppingListTool() error = nil, want missing product_group_ids error")
			}
			if backend.removeShoppingListCalls != 0 {
				t.Fatalf("remove calls = %d, want none", backend.removeShoppingListCalls)
			}
		})
	}
}

func TestChangeShoppingListToolACIDToolsAnnotations2And4To6MarksDestructiveOpenWorldTool(t *testing.T) {
	tool := changeShoppingListTool()
	if tool.Name != changeShoppingListToolName {
		t.Fatalf("tool name = %q, want %q", tool.Name, changeShoppingListToolName)
	}
	if tool.Annotations == nil {
		t.Fatal("tool annotations nil")
	}
	if tool.Annotations.ReadOnlyHint {
		t.Fatal("change_shopping_list read-only hint = true, want false")
	}
	if tool.Annotations.DestructiveHint == nil || !*tool.Annotations.DestructiveHint {
		t.Fatalf("change_shopping_list destructive hint = %#v, want true", tool.Annotations.DestructiveHint)
	}
	if tool.Annotations.OpenWorldHint == nil || !*tool.Annotations.OpenWorldHint {
		t.Fatalf("change_shopping_list open-world hint = %#v, want true", tool.Annotations.OpenWorldHint)
	}
	if tool.Annotations.Title == "" {
		t.Fatal("change_shopping_list annotation title empty")
	}
	if !strings.Contains(tool.Description, "clear") || !strings.Contains(tool.Description, "destructive") {
		t.Fatalf("change_shopping_list description = %q, want clear/destructive warning", tool.Description)
	}
}
