using System.Text.Json;
using System.Text.RegularExpressions;
using ErsatzTV.Mcp;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Mcp.Tests;

[TestFixture]
public class ToolCatalogTests
{
    [Test]
    public void All_Should_Expose_Read_Tools_For_The_Six_Families_And_Discovery()
    {
        string[] names = ToolCatalog.All.Select(t => t.Name).ToArray();

        names.ShouldContain("ersatztv_list_channels");
        names.ShouldContain("ersatztv_get_channel");
        names.ShouldContain("ersatztv_list_collections");
        names.ShouldContain("ersatztv_get_collection");
        names.ShouldContain("ersatztv_get_collection_items");
        names.ShouldContain("ersatztv_list_smart_collections");
        names.ShouldContain("ersatztv_list_schedules");
        names.ShouldContain("ersatztv_get_schedule_items");
        names.ShouldContain("ersatztv_list_playouts");
        names.ShouldContain("ersatztv_get_playout");
        names.ShouldContain("ersatztv_list_ffmpeg_profiles");
        names.ShouldContain("ersatztv_get_version");
        names.ShouldContain("ersatztv_list_media_sources");
        // Discovery reads for populating collections (#487).
        names.ShouldContain("ersatztv_search_all_items");
        names.ShouldContain("ersatztv_search_artists");
    }

    [Test]
    public void All_Should_Expose_Cautious_Write_Tools()
    {
        string[] names = ToolCatalog.All.Select(t => t.Name).ToArray();

        names.ShouldContain("ersatztv_create_collection");
        names.ShouldContain("ersatztv_update_collection");
        names.ShouldContain("ersatztv_delete_collection");
        names.ShouldContain("ersatztv_add_collection_items");
        names.ShouldContain("ersatztv_remove_collection_item");
        names.ShouldContain("ersatztv_update_collection_custom_order");
        names.ShouldContain("ersatztv_create_smart_collection");
        names.ShouldContain("ersatztv_create_schedule");
        names.ShouldContain("ersatztv_create_playout");
        names.ShouldContain("ersatztv_delete_playout");
        names.ShouldContain("ersatztv_create_channel");
        names.ShouldContain("ersatztv_update_channel");
        names.ShouldContain("ersatztv_delete_channel");
        names.ShouldContain("ersatztv_enable_jellyfin_library_sync");
        names.ShouldContain("ersatztv_scan_library");
    }

    [Test]
    public void Create_And_Update_Channel_Should_Share_The_Body_Fields_And_Require_Essentials()
    {
        ToolDefinition create = ToolCatalog.Find("ersatztv_create_channel").ShouldNotBeNull();
        ToolDefinition update = ToolCatalog.Find("ersatztv_update_channel").ShouldNotBeNull();

        create.HttpMethod.ShouldBe(HttpMethod.Post);
        update.HttpMethod.ShouldBe(HttpMethod.Put);
        update.PathTemplate.ShouldBe("/api/v1/channels/{id}");

        JsonElement createProps = create.InputSchema.RootElement.GetProperty("properties");
        createProps.TryGetProperty("name", out _).ShouldBeTrue();
        createProps.TryGetProperty("ffmpegProfileId", out _).ShouldBeTrue();
        createProps.TryGetProperty("streamingMode", out _).ShouldBeTrue();

        var createRequired = create.InputSchema.RootElement.GetProperty("required")
            .EnumerateArray().Select(e => e.GetString()).ToHashSet();
        createRequired.ShouldContain("name");
        createRequired.ShouldContain("number");
        createRequired.ShouldContain("ffmpegProfileId");
        // Enums must NOT be forced required (they have server-side defaults).
        createRequired.ShouldNotContain("streamingMode");

        // Update carries the same body fields plus the route id.
        update.InputSchema.RootElement.GetProperty("properties").TryGetProperty("id", out _).ShouldBeTrue();
        update.InputSchema.RootElement.GetProperty("properties").TryGetProperty("showInEpg", out _).ShouldBeTrue();
    }

    [Test]
    public void All_Should_Not_Expose_Deferred_Redesign_Workflow_Tools()
    {
        string[] names = ToolCatalog.All.Select(t => t.Name).ToArray();

        names.ShouldNotContain("ersatztv_create_channel_from_lineup");
        names.ShouldNotContain("ersatztv_list_channel_templates");
        names.ShouldNotContain("ersatztv_browse_library");
        names.ShouldNotContain("ersatztv_upload_channel_logo");
        names.ShouldNotContain("ersatztv_resume_playback");
    }

    [Test]
    public void All_Should_Not_Expose_Deferred_Large_Dto_Writes()
    {
        // Deferred as too-large for a cautious v0.1 (documented in docs/mcp.md): the ~40-field
        // replace-list writes.
        string[] names = ToolCatalog.All.Select(t => t.Name).ToArray();

        names.ShouldNotContain("ersatztv_replace_schedule_items");
        names.ShouldNotContain("ersatztv_replace_playout_templates");
    }

    [Test]
    public void Every_Tool_Path_Should_Be_Versioned()
    {
        foreach (ToolDefinition tool in ToolCatalog.All)
        {
            tool.PathTemplate.ShouldStartWith("/api/v1/", customMessage: tool.Name);
        }
    }

    [Test]
    public void Tool_Names_Should_Be_Unique()
    {
        ToolCatalog.All
            .GroupBy(t => t.Name, StringComparer.Ordinal)
            .Where(g => g.Count() > 1)
            .Select(g => g.Key)
            .ShouldBeEmpty();
    }

    [Test]
    public void Every_Path_Parameter_Should_Be_A_Required_Declared_Property()
    {
        foreach (ToolDefinition tool in ToolCatalog.All)
        {
            JsonElement schema = tool.InputSchema.RootElement;
            bool hasProps = schema.TryGetProperty("properties", out JsonElement properties);
            var required = schema.TryGetProperty("required", out JsonElement req)
                ? req.EnumerateArray().Select(e => e.GetString()).ToHashSet()
                : new HashSet<string?>();

            foreach (Match match in Regex.Matches(tool.PathTemplate, @"\{([^}]+)\}"))
            {
                string name = match.Groups[1].Value;
                hasProps.ShouldBeTrue($"{tool.Name}: path param {name} needs a properties block");
                properties.TryGetProperty(name, out _).ShouldBeTrue($"{tool.Name}: path param {name} not declared");
                required.ShouldContain(name, $"{tool.Name}: path param {name} must be required");
            }
        }
    }

    [Test]
    public void Every_Query_Parameter_Should_Be_A_Declared_Property()
    {
        foreach (ToolDefinition tool in ToolCatalog.All.Where(t => t.QueryParameters is { Count: > 0 }))
        {
            JsonElement properties = tool.InputSchema.RootElement.GetProperty("properties");
            foreach (string name in tool.QueryParameters!)
            {
                properties.TryGetProperty(name, out _).ShouldBeTrue($"{tool.Name}: query param {name} not declared");
            }
        }
    }

    [Test]
    public void Every_Non_Empty_Schema_Should_Forbid_Additional_Properties()
    {
        foreach (ToolDefinition tool in ToolCatalog.All)
        {
            JsonElement schema = tool.InputSchema.RootElement;
            schema.GetProperty("additionalProperties").ValueKind.ShouldBe(JsonValueKind.False, tool.Name);
        }
    }

    [Test]
    public void Get_Channel_Tool_Should_Have_OpenApi_Aligned_Path_And_Id_Input()
    {
        ToolDefinition tool = ToolCatalog.Find("ersatztv_get_channel").ShouldNotBeNull();

        tool.HttpMethod.ShouldBe(HttpMethod.Get);
        tool.PathTemplate.ShouldBe("/api/v1/channels/{id}");
        tool.InputSchema.RootElement.GetProperty("required").EnumerateArray().Single().GetString().ShouldBe("id");
        tool.InputSchema.RootElement.GetProperty("properties").TryGetProperty("id", out _).ShouldBeTrue();
    }

    [Test]
    public void Add_Collection_Items_Tool_Should_Post_With_Array_Buckets()
    {
        ToolDefinition tool = ToolCatalog.Find("ersatztv_add_collection_items").ShouldNotBeNull();

        tool.HttpMethod.ShouldBe(HttpMethod.Post);
        tool.PathTemplate.ShouldBe("/api/v1/collections/{id}/items");
        JsonElement artistIds = tool.InputSchema.RootElement.GetProperty("properties").GetProperty("artistIds");
        artistIds.GetProperty("type").GetString().ShouldBe("array");
        artistIds.GetProperty("items").GetProperty("type").GetString().ShouldBe("integer");
    }

    [Test]
    public void Custom_Order_Tool_Should_Declare_IfMatch_Header_Argument()
    {
        ToolDefinition tool = ToolCatalog.Find("ersatztv_update_collection_custom_order").ShouldNotBeNull();

        tool.HttpMethod.ShouldBe(HttpMethod.Put);
        tool.InputSchema.RootElement.GetProperty("properties").TryGetProperty("ifMatch", out _).ShouldBeTrue();
        // ifMatch is a header, not a query parameter.
        (tool.QueryParameters ?? new HashSet<string>()).ShouldNotContain("ifMatch");
    }

    [Test]
    public void Scan_Library_Tool_Should_Register_Deep_As_A_Query_Parameter()
    {
        ToolDefinition tool = ToolCatalog.Find("ersatztv_scan_library").ShouldNotBeNull();

        tool.HttpMethod.ShouldBe(HttpMethod.Post);
        tool.QueryParameters.ShouldNotBeNull();
        tool.QueryParameters!.ShouldContain("deep");
    }
}
