using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using GerbilManagerWebAPI.Cms;

namespace GerbilManager.Tests;

/// <summary>
/// WEB-1: static site renderer — snapshot → HTML file map.
/// All tests are pure unit tests (no DB, no HTTP); they build JsonObject
/// snapshots directly and assert on rendered HTML.
/// </summary>
public class SiteRendererTests
{
    // ── Snapshot builder helpers ─────────────────────────────────────────────

    private static JsonObject MakeSnapshot(params (string slug, string title, JsonArray blocks)[] pages)
    {
        var navOrder = new JsonArray();
        var pagesArr = new JsonArray();
        foreach (var (slug, title, blocks) in pages)
        {
            navOrder.Add(slug);
            pagesArr.Add(new JsonObject
            {
                ["slug"] = slug,
                ["title"] = title,
                ["seoDescription"] = $"SEO für {title}",
                ["status"] = "Published",
                ["blocks"] = blocks,
            });
        }
        return new JsonObject
        {
            ["site"] = new JsonObject { ["defaultLocale"] = "de", ["navOrder"] = navOrder },
            ["pages"] = pagesArr,
        };
    }

    private static JsonArray Blocks(params JsonObject[] blocks)
    {
        var arr = new JsonArray();
        int i = 0;
        foreach (var b in blocks) { b["order"] = i++; arr.Add(b); }
        return arr;
    }

    private static JsonObject HeadingBlock(string text, int level = 1) => new()
    {
        ["type"] = "Heading",
        ["data"] = new JsonObject { ["text"] = text, ["level"] = level },
    };

    private static JsonObject RichTextBlock(string md) => new()
    {
        ["type"] = "RichText",
        ["data"] = new JsonObject { ["markdown"] = md },
    };

    private static JsonObject ImageBlock(string url, string alt) => new()
    {
        ["type"] = "Image",
        ["data"] = new JsonObject { ["url"] = url, ["alt"] = alt },
    };

    private static JsonObject ContactInfoBlock(string name, string email, string phone) => new()
    {
        ["type"] = "ContactInfo",
        ["data"] = new JsonObject { ["name"] = name, ["email"] = email, ["phone"] = phone },
    };

    private static JsonObject AbgabetiereBlock(string intro,
        params (string name, string farbe, string? group, string? photo, string? gender, string? dob, string? note)[] animals)
    {
        var animalArr = new JsonArray();
        foreach (var (name, farbe, group, photo, gender, dob, note) in animals)
        {
            var photos = new JsonArray();
            if (photo is not null) photos.Add(photo);
            animalArr.Add(new JsonObject
            {
                ["name"] = name,
                ["farbschlag"] = farbe,
                ["group"] = group,
                ["gender"] = gender,
                ["dateOfBirth"] = dob,
                ["characterNote"] = note,
                ["photos"] = photos,
                ["aiSaleText"] = (JsonNode?)null,
            });
        }
        return new JsonObject
        {
            ["type"] = "AbgabetiereList",
            ["data"] = new JsonObject { ["mode"] = "auto", ["intro"] = intro, ["animals"] = animalArr },
        };
    }


    // ── Tests ────────────────────────────────────────────────────────────────

    [Fact]
    public void Render_produces_index_html_for_start_page()
    {
        var snap = MakeSnapshot(("start", "Startseite", Blocks(HeadingBlock("Willkommen"))));
        var files = SiteRenderer.Render(snap);
        Assert.True(files.ContainsKey("index.html"));
    }

    [Fact]
    public void Render_produces_slug_subfolder_for_non_start_pages()
    {
        var snap = MakeSnapshot(
            ("start", "Start", Blocks()),
            ("abgabetiere", "Abgabetiere", Blocks()));
        var files = SiteRenderer.Render(snap);
        Assert.True(files.ContainsKey("abgabetiere/index.html"));
    }

    [Fact]
    public void Render_includes_css_asset()
    {
        var snap = MakeSnapshot(("start", "Start", Blocks()));
        var files = SiteRenderer.Render(snap);
        Assert.True(files.ContainsKey(SiteRenderer.CssPath));
        Assert.Contains("site-header", files[SiteRenderer.CssPath]);
    }

    [Fact]
    public void Page_html_is_valid_doctype_and_contains_title()
    {
        var snap = MakeSnapshot(("start", "Startseite", Blocks(HeadingBlock("Hallo"))));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.StartsWith("<!DOCTYPE html>", html);
        Assert.Contains("<title>Startseite |", html);
        Assert.Contains("lang=\"de\"", html);
    }

    [Fact]
    public void Page_html_includes_seo_description()
    {
        var snap = MakeSnapshot(("start", "Start", Blocks()));
        // seoDescription is set to "SEO für Start" by our helper
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("meta name=\"description\"", html);
        Assert.Contains("SEO für Start", html);
    }

    [Fact]
    public void Nav_links_are_rendered_with_all_slugs()
    {
        var snap = MakeSnapshot(
            ("start", "Start", Blocks()),
            ("abgabetiere", "Abgabetiere", Blocks()),
            ("kontakt", "Kontakt", Blocks()));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("Abgabetiere", html);
        Assert.Contains("Kontakt", html);
    }

    [Fact]
    public void Current_page_nav_link_has_aria_current()
    {
        var snap = MakeSnapshot(
            ("start", "Start", Blocks()),
            ("abgabetiere", "Abgabetiere", Blocks()));
        var abgHtml = SiteRenderer.Render(snap)["abgabetiere/index.html"];
        Assert.Contains("aria-current=\"page\"", abgHtml);
    }

    [Fact]
    public void Heading_block_renders_h_tag_with_text()
    {
        var snap = MakeSnapshot(("start", "Start", Blocks(HeadingBlock("Meine Rennmäuse", 2))));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("<h2", html);
        Assert.Contains("Meine Rennmäuse", html);
    }

    [Fact]
    public void RichText_block_renders_markdown_bold_and_paragraph()
    {
        var snap = MakeSnapshot(("start", "Start", Blocks(RichTextBlock("**Hallo** Welt"))));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("<strong>Hallo</strong>", html);
    }

    [Fact]
    public void RichText_block_renders_markdown_list()
    {
        var snap = MakeSnapshot(("start", "Start", Blocks(RichTextBlock("- Eins\n- Zwei"))));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("<ul>", html);
        Assert.Contains("<li>", html);
        Assert.Contains("Eins", html);
    }

    [Fact]
    public void Image_block_renders_img_with_alt()
    {
        var snap = MakeSnapshot(("start", "Start", Blocks(ImageBlock("/img/maus.jpg", "Eine Rennmaus"))));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("src=\"/img/maus.jpg\"", html);
        Assert.Contains("alt=\"Eine Rennmaus\"", html);
    }

    [Fact]
    public void ContactInfo_block_renders_name_and_email()
    {
        var snap = MakeSnapshot(("kontakt", "Kontakt",
            Blocks(ContactInfoBlock("Kleine Chaoten", "info@kleine-chaoten.de", "+49 123 456789"))));
        var html = SiteRenderer.Render(snap)["kontakt/index.html"];
        Assert.Contains("Kleine Chaoten", html);
        Assert.Contains("info@kleine-chaoten.de", html);
        Assert.Contains("+49 123 456789", html);
    }

    [Fact]
    public void AbgabetiereList_renders_group_card_with_name_farbschlag_and_photo()
    {
        var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
            Blocks(AbgabetiereBlock("Aktuelle Tiere:", ("Krümel", "CP-Agouti", "Großbecken", "/photos/files/abc.jpg", null, null, null)))));
        var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
        Assert.Contains("Krümel", html);
        Assert.Contains("CP-Agouti", html);
        Assert.Contains("Großbecken", html);
        Assert.Contains("/photos/files/abc.jpg", html);
        Assert.Contains("cms-group-card", html);
    }

    [Fact]
    public void AbgabetiereList_group_card_shows_gender_label_and_available_badge()
    {
        var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
            Blocks(AbgabetiereBlock("",
                ("Frieda", "Agouti", "Becken 1", null, "female", null, null),
                ("Rosa", "PEW", "Becken 1", null, "female", null, null)))));
        var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
        Assert.Contains("Weibchen", html);
        Assert.Contains("Verfügbar", html);
        Assert.Contains("cms-badge--free", html);
    }

    [Fact]
    public void AbgabetiereList_shows_dateOfBirth_and_characterNote()
    {
        var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
            Blocks(AbgabetiereBlock("",
                ("Pünktchen", "Siamesisch", null, null, "female", "2024-03-15", "Sehr neugierig und verspielt.")))));
        var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
        Assert.Contains("März 2024", html);
        Assert.Contains("Sehr neugierig und verspielt.", html);
    }

    [Fact]
    public void AbgabetiereList_solo_animal_without_group_gets_its_own_card()
    {
        var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
            Blocks(AbgabetiereBlock("",
                ("Einzelkind", "Zobel", null, null, "male", null, null)))));
        var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
        Assert.Contains("Einzelkind", html);
        Assert.Contains("Männchen", html);
        Assert.Contains("cms-group-card", html);
    }

    [Fact]
    public void AbgabetiereList_empty_shows_placeholder_text()
    {
        var snap = MakeSnapshot(("abgabetiere", "Abgabetiere",
            Blocks(AbgabetiereBlock(""))));
        var html = SiteRenderer.Render(snap)["abgabetiere/index.html"];
        Assert.Contains("cms-abgabe-empty", html);
    }

    [Fact]
    public void Draft_pages_are_excluded_from_output()
    {
        var pages = new JsonArray
        {
            new JsonObject { ["slug"] = "start", ["title"] = "Start", ["status"] = "Published",
                ["seoDescription"] = (JsonNode?)null, ["blocks"] = new JsonArray() },
            new JsonObject { ["slug"] = "entwurf", ["title"] = "Entwurf", ["status"] = "Draft",
                ["seoDescription"] = (JsonNode?)null, ["blocks"] = new JsonArray() },
        };
        var snap = new JsonObject
        {
            ["site"] = new JsonObject { ["defaultLocale"] = "de", ["navOrder"] = new JsonArray { "start", "entwurf" } },
            ["pages"] = pages,
        };
        var files = SiteRenderer.Render(snap);
        Assert.True(files.ContainsKey("index.html"));
        Assert.False(files.ContainsKey("entwurf/index.html"));
    }

    [Fact]
    public void Html_encoding_prevents_xss_in_page_title()
    {
        var snap = MakeSnapshot(("start", "<script>alert(1)</script>", Blocks()));
        var html = SiteRenderer.Render(snap)["index.html"];
        Assert.DoesNotContain("<script>", html);
        Assert.Contains("&lt;script&gt;", html);
    }

    [Fact]
    public void Render_six_seeded_pages_produces_expected_paths()
    {
        var snap = MakeSnapshot(
            ("start", "Startseite", Blocks()),
            ("ueber-die-zucht", "Über die Zucht", Blocks()),
            ("abgabetiere", "Abgabetiere", Blocks(AbgabetiereBlock(""))),
            ("abgabebedingungen", "Abgabebedingungen", Blocks()),
            ("farben-genetik", "Farben & Genetik", Blocks()),
            ("kontakt", "Kontakt", Blocks()));
        var files = SiteRenderer.Render(snap);
        Assert.True(files.ContainsKey("index.html"));
        Assert.True(files.ContainsKey("ueber-die-zucht/index.html"));
        Assert.True(files.ContainsKey("abgabetiere/index.html"));
        Assert.True(files.ContainsKey("abgabebedingungen/index.html"));
        Assert.True(files.ContainsKey("farben-genetik/index.html"));
        Assert.True(files.ContainsKey("kontakt/index.html"));
        Assert.True(files.ContainsKey(SiteRenderer.CssPath));
        // 6 pages + 1 CSS
        Assert.Equal(7, files.Count);
    }

    [Fact]
    public void Render_impressum_and_datenschutz_pages_are_rendered()
    {
        var snap = MakeSnapshot(
            ("start", "Startseite", Blocks()),
            ("impressum", "Impressum", Blocks(RichTextBlock("§ 5 TMG Platzhalter"))),
            ("datenschutz", "Datenschutz", Blocks(RichTextBlock("Datenschutz Platzhalter"))));
        var files = SiteRenderer.Render(snap);
        Assert.True(files.ContainsKey("impressum/index.html"));
        Assert.True(files.ContainsKey("datenschutz/index.html"));
        Assert.Contains("§ 5 TMG Platzhalter", files["impressum/index.html"]);
    }

    [Fact]
    public void Footer_contains_impressum_and_datenschutz_links()
    {
        var snap = MakeSnapshot(
            ("start", "Startseite", Blocks()),
            ("kontakt", "Kontakt", Blocks()));
        var startHtml = SiteRenderer.Render(snap)["index.html"];
        Assert.Contains("impressum/index.html", startHtml);
        Assert.Contains("datenschutz/index.html", startHtml);
        // links in footer, not nav
        Assert.Contains("site-footer-link", startHtml);
    }

    [Fact]
    public void Footer_links_use_correct_relative_path_from_subpage()
    {
        var snap = MakeSnapshot(
            ("start", "Startseite", Blocks()),
            ("kontakt", "Kontakt", Blocks()));
        var kontaktHtml = SiteRenderer.Render(snap)["kontakt/index.html"];
        // subpage needs ../ prefix for footer links
        Assert.Contains("../impressum/index.html", kontaktHtml);
        Assert.Contains("../datenschutz/index.html", kontaktHtml);
    }

    // ── Markdown tests ───────────────────────────────────────────────────────

    [Theory]
    [InlineData("**fett**", "<strong>fett</strong>")]
    [InlineData("*kursiv*", "<em>kursiv</em>")]
    [InlineData("`code`", "<code>code</code>")]
    public void Markdown_inline_renders_correctly(string md, string expectedFragment)
    {
        var html = SiteRenderer.MarkdownToHtml(md);
        Assert.Contains(expectedFragment, html);
    }

    [Fact]
    public void Markdown_heading_renders_h_tag()
    {
        var html = SiteRenderer.MarkdownToHtml("## Abschnitt");
        Assert.Contains("<h2>Abschnitt</h2>", html);
    }

    [Fact]
    public void Markdown_xss_text_is_encoded()
    {
        var html = SiteRenderer.MarkdownToHtml("<script>alert(1)</script>");
        Assert.DoesNotContain("<script>", html);
    }
}

/// <summary>WEB-1: smoke-test the /api/render-site endpoint via ApiFactory.</summary>
public class RenderSiteEndpointTests : IClassFixture<ApiFactory>
{
    private readonly HttpClient _client;
    public RenderSiteEndpointTests(ApiFactory factory) => _client = factory.CreateClient();

    [Fact]
    public async Task RenderSite_returns_file_list_with_css_and_pages()
    {
        var json = await _client.GetStringAsync("/api/render-site");
        var files = JsonSerializer.Deserialize<List<RenderEntry>>(json,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
        Assert.Contains(files, f => f.Path == SiteRenderer.CssPath);
        Assert.Contains(files, f => f.Path == "index.html");
        Assert.All(files, f => Assert.True(f.Size > 0));
    }

    private record RenderEntry(string Path, int Size);
}
