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

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

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

type fakeBackend struct {
	shoppingList     cooked.ShoppingList
	recipes          []cooked.RecipeCard
	searchRecipes    []cooked.RecipeCard
	recipeMetadata   cooked.RecipeMetadata
	recipeContent    cooked.RecipeContent
	page             int
	limit            int
	searchQuery      string
	searchPage       int
	metadataRecipeID string
	contentRecipeID  string
	metadataCalls    int
	contentCalls     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
}
