using Microsoft.AspNetCore.Http;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests;

[TestFixture]
public class LegacyUiRedirectsTests
{
    private static readonly string StartupSource = File.ReadAllText(FindStartupPath());

    [Test]
    public void Every_Mapping_Should_Resolve()
    {
        foreach ((string from, string to) in LegacyUiRedirects.Map)
        {
            LegacyUiRedirects.TryGetRedirect(new PathString(from), out string target).ShouldBeTrue();
            target.ShouldBe(to);
        }
    }

    [TestCase("/", "/app")]
    [TestCase("/channels", "/app/channels")]
    [TestCase("/channels/add", "/app/new-channel")]
    [TestCase("/schedules", "/app/schedules")]
    [TestCase("/playouts", "/app/playouts")]
    [TestCase("/media/libraries", "/app/libraries")]
    [TestCase("/settings/ffmpeg", "/app/settings/streaming")]
    [TestCase("/settings/hdhr", "/app/settings/system")]
    [TestCase("/settings/logging", "/app/settings/logging")]
    [TestCase("/settings/playout", "/app/settings/playout")]
    [TestCase("/settings/scanner", "/app/settings/scanner")]
    [TestCase("/settings/ui", "/app/settings/general")]
    [TestCase("/settings/xmltv", "/app/settings/xmltv")]
    // (A) parameterless routes migrated in #204 (one spot-check per new group)
    [TestCase("/channels/numbers", "/app/channels")]
    [TestCase("/system/troubleshooting/block-playout", "/app/troubleshooting/blocks")]
    [TestCase("/media/browser/images", "/app/media/images/browser")]
    [TestCase("/schedules/add", "/app/schedules")]
    // (B) ".../add" sub-actions
    [TestCase("/playouts/add", "/app/playouts")]
    [TestCase("/ffmpeg/add", "/app/ffmpeg-profiles")]
    // (E-base) browse roots
    [TestCase("/media/movies", "/app/media?kind=movies")]
    [TestCase("/media/remote/streams", "/app/media?kind=remote-streams")]
    public void Known_Route_Should_Redirect(string path, string expected)
    {
        LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
        target.ShouldBe(expected);
    }

    // (C) id-substituting
    [TestCase("/channels/5", "/app/edit-channel/5")]
    [TestCase("/blocks/12", "/app/blocks/12")]
    [TestCase("/deco-templates/3", "/app/deco-templates/3")]
    [TestCase("/media/trakt/lists/11", "/app/trakt-lists/11")]
    [TestCase("/media/movies/42", "/app/media/movies/42")]
    [TestCase("/media/tv/shows/7", "/app/media/shows/7")]
    [TestCase("/media/tv/seasons/8", "/app/media/seasons/8")]
    [TestCase("/media/music/artists/9", "/app/media/artists/9")]
    // (C2) mid-path
    [TestCase("/playouts/9/templates", "/app/playouts/9/templates")]
    [TestCase("/playouts/9/alternate-schedules", "/app/playouts/9/alternate-schedules")]
    // (D) id-dropping
    [TestCase("/schedules/4", "/app/schedules")]
    [TestCase("/schedules/4/items", "/app/schedules")]
    [TestCase("/playouts/classic/2", "/app/playouts")]
    [TestCase("/playouts/block/2", "/app/playouts")]
    [TestCase("/playouts/scripted/2", "/app/playouts")]
    [TestCase("/playouts/sequential/2", "/app/playouts")]
    [TestCase("/playouts/add/classic", "/app/playouts")]
    [TestCase("/media/collections/8", "/app/collections")]
    [TestCase("/media/collections/8/edit", "/app/collections")]
    [TestCase("/media/multi-collections/8/edit", "/app/multi-collections")]
    [TestCase("/media/rerun-collections/8/edit", "/app/rerun-collections")]
    [TestCase("/media/filler/presets/8/edit", "/app/filler-presets")]
    [TestCase("/ffmpeg/6", "/app/ffmpeg-profiles")]
    [TestCase("/watermarks/6", "/app/watermarks")]
    // (E-page) paging
    [TestCase("/media/movies/page/3", "/app/media?kind=movies")]
    [TestCase("/media/tv/shows/page/2", "/app/media?kind=shows")]
    [TestCase("/media/tv/seasons/page/2", "/app/media?kind=seasons")]
    [TestCase("/media/tv/episodes/page/2", "/app/media?kind=episodes")]
    [TestCase("/media/music/artists/page/2", "/app/media?kind=artists")]
    [TestCase("/media/music/videos/page/2", "/app/media?kind=music-videos")]
    [TestCase("/media/music/songs/page/2", "/app/media?kind=songs")]
    [TestCase("/media/other/videos/page/2", "/app/media?kind=other-videos")]
    [TestCase("/media/remote/streams/page/2", "/app/media?kind=remote-streams")]
    [TestCase("/media/images/page/2", "/app/media?kind=images")]
    public void Pattern_Route_Should_Redirect(string path, string expected)
    {
        LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
        target.ShouldBe(expected);
    }

    [TestCase("/channels/", "/app/channels")]
    [TestCase("/schedules/", "/app/schedules")]
    [TestCase("/settings/ffmpeg/", "/app/settings/streaming")]
    // trailing slash on parameterized/pattern routes
    [TestCase("/channels/5/", "/app/edit-channel/5")]
    [TestCase("/playouts/9/templates/", "/app/playouts/9/templates")]
    [TestCase("/media/movies/", "/app/media?kind=movies")]
    public void Trailing_Slash_Should_Match(string path, string expected)
    {
        LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
        target.ShouldBe(expected);
    }

    [Test]
    public void Lookup_Should_Be_Case_Insensitive()
    {
        LegacyUiRedirects.TryGetRedirect(new PathString("/Channels"), out string target).ShouldBeTrue();
        target.ShouldBe("/app/channels");
    }

    [TestCase("/Channels/5", "/app/edit-channel/5")]
    [TestCase("/MEDIA/TV/SHOWS/7", "/app/media/shows/7")]
    public void Pattern_Lookup_Should_Be_Case_Insensitive(string path, string expected)
    {
        LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
        target.ShouldBe(expected);
    }

    // #202 replaced the Blazor /media/sources/* pages with the /app/libraries SPA screens;
    // #91b deletes the Blazor pages and adds these redirects (the #204-era prefix ban is lifted).
    [TestCase("/media/sources/local", "/app/libraries")]
    [TestCase("/media/sources/local/add", "/app/libraries/local/new")]
    [TestCase("/media/sources/local/5/edit", "/app/libraries/local/5")]
    [TestCase("/media/sources/plex", "/app/libraries/plex")]
    [TestCase("/media/sources/plex/5/libraries", "/app/libraries/plex/5/sync")]
    [TestCase("/media/sources/plex/5/paths", "/app/libraries/plex/5/path-replacements")]
    [TestCase("/media/sources/jellyfin", "/app/libraries/jellyfin")]
    [TestCase("/media/sources/jellyfin/edit", "/app/libraries/jellyfin/connection")]
    [TestCase("/media/sources/jellyfin/5/libraries", "/app/libraries/jellyfin/5/sync")]
    [TestCase("/media/sources/jellyfin/5/paths", "/app/libraries/jellyfin/5/path-replacements")]
    [TestCase("/media/sources/emby", "/app/libraries/emby")]
    [TestCase("/media/sources/emby/edit", "/app/libraries/emby/connection")]
    [TestCase("/media/sources/emby/5/libraries", "/app/libraries/emby/5/sync")]
    [TestCase("/media/sources/emby/5/paths", "/app/libraries/emby/5/path-replacements")]
    public void Media_Sources_Routes_Should_Redirect_To_Libraries_Screens(string path, string expected)
    {
        LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeTrue();
        target.ShouldBe(expected);
    }

    [TestCase("/system/health")]     // former Blazor home; not mapped (Startup catch-all -> /app)
    [TestCase("/media/sources")]     // bare prefix, no such route/screen
    [TestCase("/media/sources/plex/paths")] // 4-seg, non-numeric id position
    [TestCase("/media/sources/plex/abc/libraries")] // id must be a positive integer
    [TestCase("/api/foo")]           // API surface
    [TestCase("/artwork/1")]         // artwork surface
    [TestCase("/docs")]              // docs surface
    [TestCase("/openapi")]           // openapi surface
    [TestCase("/app")]               // already the SPA
    [TestCase("/app/channels")]      // already the SPA
    [TestCase("/iptv/channels.m3u")] // IPTV surface
    [TestCase("/api/health")]        // API surface
    [TestCase("")]                   // empty
    [TestCase("//")]                 // all-slash path must not collapse to root "/"
    [TestCase("/channels//")]        // double trailing slash is not normalized to a match
    // strict id validation — these must all fall through
    [TestCase("/channels/abc")]
    [TestCase("/channels/0")]
    [TestCase("/channels/-1")]
    [TestCase("/channels/1.5")]
    [TestCase("/channels/999999999999")]
    [TestCase("/channels//5")]        // empty segment: must not match /channels/{id}
    [TestCase("/media/movies/page/abc")]
    [TestCase("/media/movies/page/0")]
    [TestCase("/playouts/templates")] // no id segment
    [TestCase("/media/movies/page")]  // 3-seg, non-numeric tail
    public void Non_Migrated_Route_Should_Not_Redirect(string path)
    {
        LegacyUiRedirects.TryGetRedirect(new PathString(path), out string target).ShouldBeFalse();
        target.ShouldBe(string.Empty);
    }

    [TestCase("/app/media?kind=movies", "?page=2", "/app/media?kind=movies&page=2")]
    [TestCase("/app/media?kind=movies", "", "/app/media?kind=movies")]
    [TestCase("/app/channels", "?x=1", "/app/channels?x=1")]
    [TestCase("/app/channels", "", "/app/channels")]
    [TestCase("/app/media?kind=movies", "?kind=shows", "/app/media?kind=movies&kind=shows")]
    public void AppendQueryString_Should_Merge(string target, string query, string expected)
    {
        LegacyUiRedirects.AppendQueryString(target, new QueryString(query)).ShouldBe(expected);
    }

    [Test]
    public void Map_Keys_Should_Not_Begin_With_Forbidden_Prefix()
    {
        string[] forbidden =
        {
            "/api", "/artwork", "/docs", "/openapi", "/iptv", "/app"
        };

        foreach (string key in LegacyUiRedirects.Map.Keys)
        {
            foreach (string prefix in forbidden)
            {
                key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
                    .ShouldBeFalse($"Map key '{key}' must not begin with forbidden prefix '{prefix}'");
            }
        }

        // The guard invariant is load-bearing for the un-prefix-guarded /api|/artwork|
        // /docs|/openapi surface, so it must cover the Tier-2 pattern templates too —
        // not only the Tier-1 Map keys — or a future prefix-violating rule would slip in.
        foreach (string template in LegacyUiRedirects.PatternTemplates)
        {
            foreach (string prefix in forbidden)
            {
                template.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
                    .ShouldBeFalse($"Pattern template '{template}' must not begin with forbidden prefix '{prefix}'");
            }
        }
    }

    [Test]
    public void Startup_Should_Redirect_Legacy_Routes_In_Legacy_Branch_Before_Routing()
    {
        // The redirect middleware must be wired inside the legacy branch and run before
        // UseRouting so migrated routes never reach the SPA catch-all fallback (#91 phase b:
        // the Blazor host is gone; unmatched non-API routes 302 to /app).
        int redirectIndex = StartupSource.IndexOf("LegacyUiRedirects.TryGetRedirect", StringComparison.Ordinal);
        redirectIndex.ShouldBeGreaterThan(-1);

        // The legacy branch's UseRouting call that follows the redirect middleware.
        int routingIndex = StartupSource.IndexOf("legacy.UseRouting()", StringComparison.Ordinal);
        routingIndex.ShouldBeGreaterThan(-1);

        redirectIndex.ShouldBeLessThan(routingIndex);

        // 302 (temporary), not a permanent redirect; incoming query merged via AppendQueryString.
        StartupSource.ShouldContain("context.Request.PathBase + LegacyUiRedirects.AppendQueryString(target");
        StartupSource.ShouldNotContain("RedirectPermanent(target");
    }

    private static string FindStartupPath()
    {
        DirectoryInfo? directory = new(TestContext.CurrentContext.TestDirectory);

        while (directory is not null)
        {
            string candidate = Path.Combine(directory.FullName, "ErsatzTV", "Startup.cs");
            if (File.Exists(candidate))
            {
                return candidate;
            }

            directory = directory.Parent;
        }

        throw new FileNotFoundException("Could not find ErsatzTV/Startup.cs");
    }
}
