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

package mcp

import (
	"context"
	"encoding/json"
	"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: "recipe-1", 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 != "recipe-1" || output.Recipes[0].Title != "Pasta" {
		t.Fatalf("first recipe = %#v, want recipe-1 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: "recipe-1", Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"},
		{ID: "recipe-2", 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 != "recipe-1" || output.Recipes[0].Title != "Pasta" {
		t.Fatalf("first recipe = %#v, want recipe-1 Pasta", output.Recipes[0])
	}
}

func TestCallToolACIDRecipesRead5ReadsSingleRecipe(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: 2,
		},
	}
	server := NewServer(backend, "test")

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

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

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

	if backend.metadataRecipeID != "recipe-1" || backend.contentRecipeID != "recipe-1" {
		t.Fatalf(
			"backend recipe IDs = metadata %q content %q, want recipe-1",
			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 != "recipe-1" || output.Recipe.Title != "Pasta" {
		t.Fatalf("structured recipe identity = %#v, want recipe-1 Pasta", output.Recipe)
	}
	if output.Recipe.Owner != "returned-user" || !output.Recipe.EditPermission {
		t.Fatalf(
			"structured recipe owner/edit = %q/%v, want returned-user/true",
			output.Recipe.Owner,
			output.Recipe.EditPermission,
		)
	}
	if output.Recipe.Content != "# Pasta\n\n- 200g pasta\n\n1. Boil pasta." || output.Recipe.Portions != 2 {
		t.Fatalf(
			"structured recipe content/portions = %q/%d, want markdown/2",
			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)
	}
	if len(result.Content) != 1 {
		t.Fatalf("text content length = %d, want 1", len(result.Content))
	}
	textContent, ok := result.Content[0].(*sdk.TextContent)
	if !ok {
		t.Fatalf("content type = %T, want *sdk.TextContent", result.Content[0])
	}
	if strings.Contains(textContent.Text, "https://example.invalid/pasta.jpg") {
		t.Fatalf("text output leaked image URL: %s", textContent.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)
	}
}

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

	_, output, err := server.callPreviewRecipeTextTool(
		context.Background(),
		nil,
		PreviewRecipeTextArguments{Title: " Pasta ", Text: "  Boil pasta.\n"},
	)
	if err != nil {
		t.Fatalf("callPreviewRecipeTextTool() 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 {
		t.Fatalf("preview output = %#v, want title/markdown/portions", output)
	}
}

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

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

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

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

func TestCallToolACIDRecipesSave2And9SavesPreparedRecipe(t *testing.T) {
	backend := &fakeBackend{saveRecipeID: "recipe-1"}
	server := NewServer(backend, "test")

	_, output, err := server.callSaveRecipeTool(
		context.Background(),
		nil,
		SaveRecipeArguments{
			Source:   " prepared ",
			Title:    " Pasta ",
			Markdown: "  # Pasta\n\n1. Boil pasta.\n",
			Portions: 2,
		},
	)
	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 {
		t.Fatalf("backend save portions = %d, want 2", backend.savePortions)
	}
	if output.RecipeID != "recipe-1" {
		t.Fatalf("save output recipe ID = %q, want recipe-1", output.RecipeID)
	}
}

func TestCallToolACIDRecipesSave2_1To2_3RequiresPreparedFields(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},
		},
		{
			name:      "portions",
			arguments: SaveRecipeArguments{Source: "prepared", Title: "Pasta", Markdown: "# Pasta", Portions: 0},
		},
	}

	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 TestCallToolACIDRecipesSave10RejectsUnsupportedSourceBeforeCallingCooked(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callSaveRecipeTool(context.Background(), nil, SaveRecipeArguments{Source: "url"})
	if err == nil {
		t.Fatal("callSaveRecipeTool() error = nil, want unsupported source 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: "recipe-1",
	}
	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 = %d, want preview portions 2", backend.savePortions)
	}
	if output.RecipeID != "recipe-1" {
		t.Fatalf("save output recipe ID = %q, want recipe-1", 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)
			}
		})
	}
}

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

	_, output, err := server.callSaveRecipeTool(
		context.Background(),
		nil,
		SaveRecipeArguments{
			Source:   " existing ",
			RecipeID: " recipe-1 ",
			Markdown: "  # Pasta\n\n1. Boil pasta.\n",
			Portions: 4,
		},
	)
	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 != "recipe-1" {
		t.Fatalf("backend update recipe ID = %q, want recipe-1", 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 {
		t.Fatalf("backend update portions = %d, want 4", backend.updatePortions)
	}
	if output.RecipeID != "recipe-1" {
		t.Fatalf("save output recipe ID = %q, want recipe-1", output.RecipeID)
	}
}

func TestCallToolACIDRecipesSave8_1To8_3RequiresExistingFields(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: "recipe-1", Markdown: "   ", Portions: 4},
		},
		{
			name:      "portions",
			arguments: SaveRecipeArguments{Source: "existing", RecipeID: "recipe-1", Markdown: "# Pasta", Portions: 0},
		},
	}

	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")

	_, output, err := server.callDeleteRecipeTool(
		context.Background(),
		nil,
		DeleteRecipeArguments{RecipeID: " recipe-1 "},
	)
	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 != "recipe-1" {
		t.Fatalf("backend delete recipe ID = %q, want recipe-1", backend.deleteRecipeID)
	}
	if output.RecipeID != "recipe-1" {
		t.Fatalf("delete output recipe ID = %q, want recipe-1", output.RecipeID)
	}
}

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")

	_, 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)
	}
}

func TestCallToolACIDShoppingListActions2RejectsUnknownActionBeforeCallingCooked(t *testing.T) {
	tests := []struct {
		name      string
		arguments ChangeShoppingListArguments
	}{
		{name: "empty", arguments: ChangeShoppingListArguments{Action: "   "}},
		{name: "unknown", arguments: ChangeShoppingListArguments{Action: "dance"}},
	}

	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 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:    " recipe-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 != "recipe-1" {
		t.Fatalf("backend recipe ID = %q, want recipe-1", 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{" pasta ", "tomato", "   "},
		},
	)
	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] != "pasta" ||
		backend.removeProductGroupIDs[1] != "tomato" {
		t.Fatalf("backend remove IDs = %#v, want pasta and tomato", backend.removeProductGroupIDs)
	}
	if len(output.RemovedProductGroupIDs) != 2 || output.RemovedProductGroupIDs[0] != "pasta" ||
		output.RemovedProductGroupIDs[1] != "tomato" {
		t.Fatalf("output removed IDs = %#v, want pasta and tomato", output.RemovedProductGroupIDs)
	}
}

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)
	}
}

type fakeBackend struct {
	shoppingList            cooked.ShoppingList
	recipes                 []cooked.RecipeCard
	searchRecipes           []cooked.RecipeCard
	recipeMetadata          cooked.RecipeMetadata
	recipeContent           cooked.RecipeContent
	preview                 cooked.RecipeTextPreview
	saveRecipeID            string
	page                    int
	limit                   int
	searchQuery             string
	searchPage              int
	metadataRecipeID        string
	contentRecipeID         string
	previewTitle            string
	previewText             string
	saveTitle               string
	saveMarkdown            string
	savePortions            int
	updateRecipeID          string
	updateMarkdown          string
	updatePortions          int
	deleteRecipeID          string
	addShoppingListResult   cooked.AddShoppingListResult
	addIngredients          string
	addRecipeID             string
	removeProductGroupIDs   []string
	metadataCalls           int
	contentCalls            int
	previewCalls            int
	saveCalls               int
	updateCalls             int
	deleteCalls             int
	clearShoppingListCalls  int
	addShoppingListCalls    int
	removeShoppingListCalls int
}

func (f *fakeBackend) ReadShoppingList(context.Context) (cooked.ShoppingList, error) {
	return f.shoppingList, nil
}

func (f *fakeBackend) ListRecipes(_ context.Context, page, limit int) ([]cooked.RecipeCard, error) {
	f.page = page
	f.limit = limit

	return f.recipes, nil
}

func (f *fakeBackend) SearchRecipes(_ context.Context, query string, page int) ([]cooked.RecipeCard, error) {
	f.searchQuery = query
	f.searchPage = page

	return f.searchRecipes, nil
}

func (f *fakeBackend) ReadRecipeMetadata(_ context.Context, recipeID string) (cooked.RecipeMetadata, error) {
	f.metadataRecipeID = recipeID
	f.metadataCalls++

	return f.recipeMetadata, nil
}

func (f *fakeBackend) ReadRecipeContent(_ context.Context, recipeID string) (cooked.RecipeContent, error) {
	f.contentRecipeID = recipeID
	f.contentCalls++

	return f.recipeContent, nil
}

func (f *fakeBackend) PreviewRecipeText(_ context.Context, title, text string) (cooked.RecipeTextPreview, error) {
	f.previewTitle = title
	f.previewText = text
	f.previewCalls++

	return f.preview, nil
}

func (f *fakeBackend) SavePreparedRecipe(_ context.Context, title, markdown string, portions int) (string, error) {
	f.saveTitle = title
	f.saveMarkdown = markdown
	f.savePortions = portions
	f.saveCalls++
	if f.saveRecipeID != "" {
		return f.saveRecipeID, nil
	}

	return "recipe-1", nil
}

func (f *fakeBackend) UpdateRecipeContent(_ context.Context, recipeID, markdown string, portions int) error {
	f.updateRecipeID = recipeID
	f.updateMarkdown = markdown
	f.updatePortions = portions
	f.updateCalls++

	return nil
}

func (f *fakeBackend) DeleteRecipe(_ context.Context, recipeID string) error {
	f.deleteRecipeID = recipeID
	f.deleteCalls++

	return nil
}

func (f *fakeBackend) ClearShoppingList(context.Context) error {
	f.clearShoppingListCalls++

	return nil
}

func (f *fakeBackend) AddShoppingListIngredients(
	_ context.Context,
	ingredients, recipeID string,
) (cooked.AddShoppingListResult, error) {
	f.addIngredients = ingredients
	f.addRecipeID = recipeID
	f.addShoppingListCalls++

	return f.addShoppingListResult, nil
}

func (f *fakeBackend) RemoveShoppingListProductGroups(_ context.Context, ids []string) error {
	f.removeProductGroupIDs = ids
	f.removeShoppingListCalls++

	return nil
}
