// 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: "add"}},
	}

	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 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
	metadataCalls          int
	contentCalls           int
	previewCalls           int
	saveCalls              int
	updateCalls            int
	deleteCalls            int
	clearShoppingListCalls 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
}
