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

package mcp

import (
	"context"
	"slices"
	"strings"
	"testing"

	"github.com/google/jsonschema-go/jsonschema"

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

// recipes.VALIDATION.1 recipes.VALIDATION.2 tools.SCHEMA.7
func TestCallToolACIDRecipesValidation1And2RejectsMalformedRecipeReadIDBeforeCooked(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipe", RecipeID: "not-a-uuid"})
	if err == nil {
		t.Fatal("CallReadTool() error = nil, want malformed 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.VALIDATION.1 recipes.VALIDATION.2
func TestCallToolACIDRecipesValidation1And2RejectsMalformedExistingRecipeIDBeforeCooked(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callSaveRecipeTool(
		context.Background(),
		nil,
		SaveRecipeArguments{Source: "existing", RecipeID: "not-a-uuid", Markdown: "# Pasta", Portions: 2},
	)
	if err == nil {
		t.Fatal("callSaveRecipeTool() error = nil, want malformed recipe_id error")
	}
	if backend.updateCalls != 0 {
		t.Fatalf("update calls = %d, want none", backend.updateCalls)
	}
}

// recipes.VALIDATION.1 recipes.VALIDATION.2
func TestCallToolACIDRecipesValidation1And2RejectsMalformedDeleteRecipeIDBeforeCooked(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

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

// tools.SURFACE.9 tools.SCHEMA.7 recipes.RESOLVE.1 recipes.RESOLVE.3
func TestCallToolACIDRecipesResolve1And3ResolvesRecipeIDPrefix(t *testing.T) {
	backend := &fakeBackend{recipes: []cooked.RecipeCard{
		{ID: testRecipeID, Title: "Pasta"},
		{ID: testOtherRecipeID, Title: "Toast"},
	}}
	server := NewServer(backend, "test")

	result, output, err := server.callResolveRecipeIDTool(
		context.Background(),
		nil,
		ResolveRecipeIDArguments{Prefix: "38f161dd-a514"},
	)
	if err != nil {
		t.Fatalf("callResolveRecipeIDTool() error = %v", err)
	}

	if backend.listRecipeCalls != 1 {
		t.Fatalf("list recipe calls = %d, want 1", backend.listRecipeCalls)
	}
	if backend.page != 1 || backend.limit != resolveRecipePageLimit {
		t.Fatalf("list page/limit = %d/%d, want 1/%d", backend.page, backend.limit, resolveRecipePageLimit)
	}
	if len(output.Recipes) != 1 || output.Recipes[0].ID != testRecipeID || output.Recipes[0].Title != "Pasta" {
		t.Fatalf("resolve output = %#v, want Pasta match", output)
	}
	if !strings.Contains(requireTextContent(t, result), testRecipeID) {
		t.Fatalf("text output = %q, want matching recipe ID", requireTextContent(t, result))
	}
}

// recipes.RESOLVE.2
func TestCallToolACIDRecipesResolve2RejectsMalformedPrefixBeforeCooked(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	_, _, err := server.callResolveRecipeIDTool(
		context.Background(),
		nil,
		ResolveRecipeIDArguments{Prefix: "pasta"},
	)
	if err == nil {
		t.Fatal("callResolveRecipeIDTool() error = nil, want malformed prefix error")
	}
	if backend.listRecipeCalls != 0 {
		t.Fatalf("list recipe calls = %d, want none", backend.listRecipeCalls)
	}
}

// recipes.RESOLVE.4
func TestCallToolACIDRecipesResolve4WarnsWhenPrefixMatchesMultipleRecipes(t *testing.T) {
	backend := &fakeBackend{recipes: []cooked.RecipeCard{
		{ID: testRecipeID, Title: "Pasta"},
		{ID: testRecipeID2, Title: "Tomato Pasta"},
	}}
	server := NewServer(backend, "test")

	result, output, err := server.callResolveRecipeIDTool(
		context.Background(),
		nil,
		ResolveRecipeIDArguments{Prefix: "38f161dd-a514-4d5a-a924-0227695d915"},
	)
	if err != nil {
		t.Fatalf("callResolveRecipeIDTool() error = %v", err)
	}
	if len(output.Recipes) != 2 {
		t.Fatalf("resolve matches = %d, want 2", len(output.Recipes))
	}
	if output.Warning == "" || !strings.Contains(requireTextContent(t, result), output.Warning) {
		t.Fatalf("warning/text = %q/%q, want ambiguity warning", output.Warning, requireTextContent(t, result))
	}
}

func TestCallToolACIDRecipesResolve4FailsWhenRecipeScanIsIncomplete(t *testing.T) {
	fullPage := make([]cooked.RecipeCard, resolveRecipePageLimit)
	for index := range fullPage {
		fullPage[index] = cooked.RecipeCard{ID: testOtherRecipeID, Title: "Toast"}
	}
	pages := make(map[int][]cooked.RecipeCard, maxResolveRecipePages+1)
	for page := 1; page <= maxResolveRecipePages+1; page++ {
		pages[page] = fullPage
	}
	backend := &fakeBackend{recipesByPage: pages}
	server := NewServer(backend, "test")

	_, _, err := server.callResolveRecipeIDTool(
		context.Background(),
		nil,
		ResolveRecipeIDArguments{Prefix: "38f161dd-a514"},
	)
	if err == nil {
		t.Fatal("callResolveRecipeIDTool() error = nil, want incomplete scan error")
	}
	if !strings.Contains(err.Error(), "too many saved recipes") {
		t.Fatalf("callResolveRecipeIDTool() error = %q, want scan cap error", err.Error())
	}
	if backend.listRecipeCalls != maxResolveRecipePages+1 {
		t.Fatalf("list recipe calls = %d, want cap plus one", backend.listRecipeCalls)
	}
}

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

// tools.SCHEMA.7 recipes.VALIDATION.1 recipes.VALIDATION.2
func TestInputSchemasACIDToolsSchema7RejectMalformedRecipeAndProductGroupIDs(t *testing.T) {
	tests := []struct {
		name      string
		schema    *jsonschema.Schema
		valid     map[string]any
		malformed map[string]any
	}{
		{
			name:      "read_recipe_id",
			schema:    readInputSchema(),
			valid:     map[string]any{"target": "recipe", "recipe_id": " " + testRecipeID + " "},
			malformed: map[string]any{"target": "recipe", "recipe_id": "not-a-uuid"},
		},
		{
			name:      "save_recipe_id",
			schema:    saveRecipeInputSchema(),
			valid:     map[string]any{"source": "existing", "recipe_id": " " + testRecipeID + " "},
			malformed: map[string]any{"source": "existing", "recipe_id": "not-a-uuid"},
		},
		{
			name:      "delete_recipe_id",
			schema:    deleteRecipeInputSchema(),
			valid:     map[string]any{"recipe_id": " " + testRecipeID + " "},
			malformed: map[string]any{"recipe_id": "not-a-uuid"},
		},
		{
			name:      "change_product_group_id",
			schema:    changeShoppingListInputSchema(),
			valid:     map[string]any{"action": "update_item", "product_group_id": " " + testProductGroupID + " "},
			malformed: map[string]any{"action": "update_item", "product_group_id": "not-a-uuid"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resolved, err := tt.schema.Resolve(nil)
			if err != nil {
				t.Fatalf("Resolve() error = %v", err)
			}
			if err := resolved.Validate(tt.valid); err != nil {
				t.Fatalf("Validate(valid) error = %v", err)
			}
			if err := resolved.Validate(tt.malformed); err == nil {
				t.Fatal("Validate(malformed) error = nil, want UUID pattern rejection")
			}
		})
	}
}

// tools.SCHEMA.7 recipes.VALIDATION.3
func TestChangeShoppingListSchemaACIDToolsSchema7AllowsMalformedBulkProductGroupIDForSkipReporting(t *testing.T) {
	resolved, err := changeShoppingListInputSchema().Resolve(nil)
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	arguments := map[string]any{
		"action":            "remove",
		"product_group_ids": []any{testProductGroupID, "not-a-uuid"},
	}
	if err := resolved.Validate(arguments); err != nil {
		t.Fatalf("Validate() with malformed bulk product_group_ids error = %v", err)
	}
}

// tools.SCHEMA.7
func TestChangeShoppingListSchemaACIDToolsSchema7AllowsOpaqueAddRecipeID(t *testing.T) {
	resolved, err := changeShoppingListInputSchema().Resolve(nil)
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if err := resolved.Validate(
		map[string]any{"action": "add", "ingredients": "200g pasta", "recipe_id": "draft-1"},
	); err != nil {
		t.Fatalf("Validate() with opaque add recipe_id error = %v", err)
	}
}

// recipes.VALIDATION.3
func TestCallToolACIDRecipesValidation3SkipsMalformedProductGroupIDsInArrays(t *testing.T) {
	backend := &fakeBackend{}
	server := NewServer(backend, "test")

	result, output, err := server.callChangeShoppingListTool(
		context.Background(),
		nil,
		ChangeShoppingListArguments{
			Action:          "remove",
			ProductGroupIDs: []string{testProductGroupID, "not-a-uuid", "  ", testProductGroupID2},
		},
	)
	if err != nil {
		t.Fatalf("callChangeShoppingListTool() error = %v", err)
	}
	if !slices.Equal(backend.removeProductGroupIDs, []string{testProductGroupID, testProductGroupID2}) {
		t.Fatalf("backend remove IDs = %#v, want valid IDs only", backend.removeProductGroupIDs)
	}
	if !slices.Equal(output.SkippedProductGroupIDs, []string{"not-a-uuid"}) {
		t.Fatalf("skipped IDs = %#v, want malformed ID reported", output.SkippedProductGroupIDs)
	}
	if !strings.Contains(requireTextContent(t, result), "not-a-uuid") {
		t.Fatalf("text output = %q, want skipped malformed ID", requireTextContent(t, result))
	}
}

// recipes.VALIDATION.3
func TestCallToolACIDRecipesValidation3ReportsMalformedOnlyProductGroupIDArraysWithoutMutation(t *testing.T) {
	tests := []string{"remove", "replace_selection", "add_selection", "remove_selection"}

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

			result, output, err := server.callChangeShoppingListTool(
				context.Background(),
				nil,
				ChangeShoppingListArguments{Action: action, ProductGroupIDs: []string{"not-a-uuid"}},
			)
			if err != nil {
				t.Fatalf("callChangeShoppingListTool() error = %v", err)
			}
			if !slices.Equal(output.SkippedProductGroupIDs, []string{"not-a-uuid"}) {
				t.Fatalf("skipped IDs = %#v, want malformed ID reported", output.SkippedProductGroupIDs)
			}
			if !strings.Contains(requireTextContent(t, result), "not-a-uuid") {
				t.Fatalf("text output = %q, want skipped malformed ID", requireTextContent(t, result))
			}
			if backend.removeShoppingListCalls != 0 || backend.readShoppingListCalls != 0 ||
				backend.replaceSelectionCalls != 0 {
				t.Fatalf(
					"backend calls remove/read/replace = %d/%d/%d, want none",
					backend.removeShoppingListCalls,
					backend.readShoppingListCalls,
					backend.replaceSelectionCalls,
				)
			}
		})
	}
}
