use super::*;
use std::fs;
use std::time::UNIX_EPOCH;
use tempfile::TempDir;

fn write_template(root: &Path, name: &str, contents: &str) {
    let path = root.join("templates").join(name);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(path, contents).unwrap();
}

fn setup_markdown_templates(root: &Path) {
    write_template(
        root,
        "base.html",
        "<!doctype html><html><body>{% block content %}{% endblock %}</body></html>",
    );
    write_template(
        root,
        "post.html",
        "{% extends \"base.html\" %}{% block content %}<article>{{ post.title }}|{{ post.body | safe }}|{{ post.date }}|{{ post.excerpt }}</article>{% endblock %}",
    );
    write_template(
        root,
        "index.html",
        "{% extends \"base.html\" %}{% block content %}<section data-current=\"{{ pagination.current }}\" data-total=\"{{ pagination.total }}\" data-prev=\"{{ pagination.prev | safe }}\" data-next=\"{{ pagination.next | safe }}\">{% for post in posts %}<article data-slug=\"{{ post.slug }}\"></article>{% endfor %}</section>{% endblock %}",
    );
    write_template(
        root,
        "tag.html",
        "{% extends \"base.html\" %}{% block content %}<section data-tag=\"{{ tag }}\" data-current=\"{{ pagination.current }}\" data-total=\"{{ pagination.total }}\" data-prev=\"{{ pagination.prev | safe }}\" data-next=\"{{ pagination.next | safe }}\">{% for post in posts %}<article data-slug=\"{{ post.slug }}\"></article>{% endfor %}</section>{% endblock %}",
    );
    write_template(
        root,
        "archive_year.html",
        "{% extends \"base.html\" %}{% block content %}<section data-year=\"{{ year }}\">{% for post in posts %}<article data-slug=\"{{ post.slug }}\"></article>{% endfor %}</section>{% endblock %}",
    );
    write_template(
        root,
        "archive_month.html",
        "{% extends \"base.html\" %}{% block content %}<section data-year=\"{{ year }}\" data-month=\"{{ month }}\">{% for post in posts %}<article data-slug=\"{{ post.slug }}\"></article>{% endfor %}</section>{% endblock %}",
    );
    write_template(
        root,
        "rss.xml",
        "{% autoescape false %}\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n  <channel>\n    <title>{{ feed.title }}</title>\n    <link>{{ feed.site_url }}</link>\n    <description>{{ feed.description }}</description>\n    <lastBuildDate>{{ feed.updated }}</lastBuildDate>\n    <generator>bckt</generator>\n    <atom:link href=\"{{ feed.feed_url }}\" rel=\"self\" type=\"application/rss+xml\"/>\n    {% for item in feed.items %}\n    <item>\n      <title>{{ item.title | default(value=item.slug) }}</title>\n      <link>{{ base_url }}{{ item.permalink }}</link>\n      <guid isPermaLink=\"true\">{{ base_url }}{{ item.permalink }}</guid>\n      <pubDate>{{ item.pub_date }}</pubDate>\n      <description>{{ item.excerpt | default(value=item.title | default(value=item.slug)) }}</description>\n      <content:encoded><![CDATA[{{ item.body }}]]></content:encoded>\n    </item>\n    {% endfor %}\n  </channel>\n</rss>\n{% endautoescape %}\n",
    );
}

fn write_markdown_post(root: &Path, body: &str) {
    let post_dir = root.join("posts/hello-world");
    fs::create_dir_all(&post_dir).unwrap();
    fs::write(
        post_dir.join("post.md"),
        format!(
            "---\ntitle: Example\ndate: 2024-01-02T03:04:05Z\ntags: [test]\n---\n{}",
            body
        ),
    )
    .unwrap();
}

fn write_tagged_post(root: &Path, slug: &str, tag: &str, date: &str, body: &str) {
    let dir = root.join("posts").join(slug);
    fs::create_dir_all(&dir).unwrap();
    fs::write(
        dir.join("post.md"),
        format!(
            "---\ntitle: {0}\ndate: {2}\nslug: {0}\ntags:\n  - {1}\n---\n{3}",
            slug, tag, date, body
        ),
    )
    .unwrap();
}

fn write_dated_post(root: &Path, slug: &str, date: &str, body: &str) {
    let dir = root.join("posts").join(slug);
    fs::create_dir_all(&dir).unwrap();
    fs::write(
        dir.join("post.md"),
        format!(
            "---\ntitle: {0}\ndate: {1}\nslug: {0}\ntags:\n  - {0}\n---\n{2}",
            slug, date, body
        ),
    )
    .unwrap();
}

fn file_mtime(path: &Path) -> std::time::Duration {
    fs::metadata(path)
        .unwrap()
        .modified()
        .unwrap()
        .duration_since(UNIX_EPOCH)
        .unwrap()
}

fn wait_for_filesystem_tick() {
    std::thread::sleep(std::time::Duration::from_millis(1100));
}

#[test]
fn renders_markdown_post_to_expected_location() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    fs::create_dir_all(root.join("skel")).unwrap();
    setup_markdown_templates(root);
    write_markdown_post(root, "Hello **world**!");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let output = root.join("html/2024/01/02/hello-world/index.html");
    let rendered = fs::read_to_string(output).unwrap();
    assert!(rendered.contains("Example"));
    assert!(rendered.contains("<strong>world</strong>"));
    assert!(rendered.contains("Hello world"));

    let homepage = fs::read_to_string(root.join("html/index.html")).unwrap();
    assert!(homepage.contains("article data-slug=\"hello-world\""));
    assert!(homepage.contains("data-current=\"1\""));
    assert!(homepage.contains("data-total=\"1\""));
}

#[test]
fn copies_post_assets() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts/assets-post")).unwrap();
    setup_markdown_templates(root);
    fs::write(
        root.join("posts/assets-post/post.md"),
        "---\ndate: 2024-01-01T00:00:00Z\nattached: [data/notes.txt, images/pic.png]\n---\nBody",
    )
    .unwrap();
    fs::create_dir_all(root.join("posts/assets-post/data")).unwrap();
    fs::create_dir_all(root.join("posts/assets-post/images")).unwrap();
    fs::write(root.join("posts/assets-post/data/notes.txt"), "notes").unwrap();
    fs::write(root.join("posts/assets-post/images/pic.png"), "image").unwrap();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let asset = root.join("html/2024/01/01/assets-post/data/notes.txt");
    let image = root.join("html/2024/01/01/assets-post/images/pic.png");
    assert!(asset.exists());
    assert!(image.exists());
}

#[test]
fn renders_pages_from_pages_directory() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    setup_markdown_templates(root);
    fs::create_dir_all(root.join("pages/about")).unwrap();
    fs::create_dir_all(root.join("pages/features/page1/assets")).unwrap();
    fs::write(
        root.join("pages/404.html"),
        "{% extends \"base.html\" %}{% block content %}<h1>Missing</h1>{% endblock %}",
    )
    .unwrap();
    fs::write(
            root.join("pages/about/index.html"),
            "{% extends \"base.html\" %}{% block content %}<p>About {{ config.title | default('site') }}</p>{% endblock %}",
        )
        .unwrap();
    fs::write(
        root.join("pages/features/page1/index.html"),
        "{% extends \"base.html\" %}{% block content %}<p>Feature</p>{% endblock %}",
    )
    .unwrap();
    fs::write(root.join("pages/features/page1/diagram.svg"), "<svg></svg>").unwrap();
    fs::write(
        root.join("pages/features/page1/assets/data.json"),
        r#"{"ok":true}"#,
    )
    .unwrap();

    render_site(
        root,
        RenderPlan {
            posts: false,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let not_found = fs::read_to_string(root.join("html/404.html")).unwrap();
    assert!(not_found.contains("Missing"));

    let about = fs::read_to_string(root.join("html/about/index.html")).unwrap();
    assert!(about.contains("About"));

    let feature = fs::read_to_string(root.join("html/features/page1/index.html")).unwrap();
    assert!(feature.contains("Feature"));
    assert_eq!(
        fs::read_to_string(root.join("html/features/page1/diagram.svg")).unwrap(),
        "<svg></svg>"
    );
    assert_eq!(
        fs::read_to_string(root.join("html/features/page1/assets/data.json")).unwrap(),
        r#"{"ok":true}"#
    );
}

#[test]
fn writes_search_index_with_posts() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    setup_markdown_templates(root);
    write_markdown_post(
        root,
        "This example body contains enough English text to exercise the search index.",
    );

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let index_path = root.join("html/assets/search/search-index.json");
    assert!(index_path.exists());
    let data = fs::read_to_string(index_path).unwrap();
    let payload: serde_json::Value = serde_json::from_str(&data).unwrap();
    assert_eq!(payload["documents"].as_array().unwrap().len(), 1);
    assert_eq!(payload["documents"][0]["language"], "en");
}

#[test]
fn search_index_updates_when_post_changes() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    setup_markdown_templates(root);
    write_markdown_post(
        root,
        "Initial body content with enough characters for indexing.",
    );

    let full_plan = RenderPlan {
        posts: true,
        static_assets: false,
        mode: BuildMode::Full,
        verbose: false,
    };
    render_site(root, full_plan).unwrap();

    let index_path = root.join("html/assets/search/search-index.json");
    let original = fs::read_to_string(&index_path).unwrap();

    fs::write(
            root.join("posts/hello-world/post.md"),
            "---\ntitle: Example\ndate: 2024-01-02T03:04:05Z\ntags: [test]\n---\nChanged body text that modifies the search index.",
        )
        .unwrap();

    let changed_plan = RenderPlan {
        posts: true,
        static_assets: false,
        mode: BuildMode::Changed,
        verbose: false,
    };
    render_site(root, changed_plan).unwrap();

    let updated = fs::read_to_string(&index_path).unwrap();
    assert_ne!(original, updated);
}

#[test]
fn exposes_additional_front_matter_in_templates() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    fs::write(
            root.join("templates/post.html"),
            "{% extends \"base.html\" %}{% block content %}<article>{{ post.location.country }}</article>{% endblock %}",
        )
        .unwrap();

    fs::create_dir_all(root.join("posts/location")).unwrap();
    fs::write(
        root.join("posts/location/post.md"),
        "---\ndate: 2024-01-01T00:00:00Z\nlocation:\n  country: GR\n---\nBody",
    )
    .unwrap();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let rendered = fs::read_to_string(root.join("html/2024/01/01/location/index.html")).unwrap();
    assert!(rendered.contains("GR"));
}

#[test]
fn copies_static_assets() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("skel/css")).unwrap();
    fs::write(root.join("skel/css/site.css"), "body { color: black; }").unwrap();
    setup_markdown_templates(root);

    render_site(
        root,
        RenderPlan {
            posts: false,
            static_assets: true,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let copied = root.join("html/css/site.css");
    assert!(copied.exists());
}

#[test]
fn paginates_homepage_with_page_numbers() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);
    fs::write(root.join("bckt.yaml"), "homepage_posts: 1\n").unwrap();

    write_dated_post(root, "alpha", "2024-01-01T00:00:00Z", "A");
    write_dated_post(root, "beta", "2024-02-01T00:00:00Z", "B");
    write_dated_post(root, "gamma", "2024-03-01T00:00:00Z", "C");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    // Posts are sorted ascending, so page 1 has alpha (oldest), homepage has gamma (newest)
    // Homepage is at the end of the pagination sequence, so prev goes backward to page 2
    let index = fs::read_to_string(root.join("html/index.html")).unwrap();
    assert!(index.contains("article data-slug=\"gamma\""));
    assert!(index.contains("data-prev=\"/page/2/\""));
    assert!(index.contains("data-next=\"\""));
    assert!(index.contains("data-current=\"3\""));
    assert!(index.contains("data-total=\"3\""));

    // Page 2 is in the middle
    let second = fs::read_to_string(root.join("html/page/2/index.html")).unwrap();
    assert!(second.contains("article data-slug=\"beta\""));
    assert!(second.contains("data-prev=\"/page/1/\""));
    assert!(second.contains("data-next=\"/\""));
    assert!(second.contains("data-current=\"2\""));
    assert!(second.contains("data-total=\"3\""));

    // Page 1 is at the beginning
    let first = fs::read_to_string(root.join("html/page/1/index.html")).unwrap();
    assert!(first.contains("article data-slug=\"alpha\""));
    assert!(first.contains("data-prev=\"\""));
    assert!(first.contains("data-next=\"/page/2/\""));
    assert!(first.contains("data-current=\"1\""));
    assert!(first.contains("data-total=\"3\""));

    // Add a new post and ensure homepage is updated but old pages remain stable
    write_dated_post(root, "delta", "2024-04-01T00:00:00Z", "D");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    // Homepage now shows delta (newest), prev goes to page 3
    let refreshed_index = fs::read_to_string(root.join("html/index.html")).unwrap();
    assert!(refreshed_index.contains("article data-slug=\"delta\""));
    assert!(refreshed_index.contains("data-prev=\"/page/3/\""));
    assert!(refreshed_index.contains("data-current=\"4\""));
    assert!(refreshed_index.contains("data-total=\"4\""));

    // Page 1 (alpha) and Page 2 (beta) should still exist and be unchanged
    assert!(root.join("html/page/1/index.html").exists());
    assert!(root.join("html/page/2/index.html").exists());
}

#[test]
fn renders_tag_pages_without_pagination() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);
    fs::write(
        root.join("bckt.yaml"),
        "homepage_posts: 5\npaginate_tags: false\n",
    )
    .unwrap();

    write_tagged_post(root, "first", "shared", "2024-01-01T00:00:00Z", "Body A");
    write_tagged_post(root, "second", "shared", "2024-02-01T00:00:00Z", "Body B");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let tag_root = root.join("html/tags/shared");
    assert!(tag_root.join("index.html").exists());
    assert!(!tag_root.join("first").exists());
}

#[test]
fn renders_tag_pages_with_pagination() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);
    fs::write(
        root.join("bckt.yaml"),
        "homepage_posts: 1\npaginate_tags: true\n",
    )
    .unwrap();

    write_tagged_post(root, "alpha", "shared", "2024-01-01T00:00:00Z", "A");
    write_tagged_post(root, "beta", "shared", "2024-02-01T00:00:00Z", "B");
    write_tagged_post(root, "gamma", "shared", "2024-03-01T00:00:00Z", "C");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let tag_index = fs::read_to_string(root.join("html/tags/shared/index.html")).unwrap();
    assert!(tag_index.contains("article data-slug=\"gamma\""));
    assert!(tag_index.contains("article data-slug=\"beta\""));
    assert!(tag_index.contains("article data-slug=\"alpha\""));
    assert!(tag_index.contains("data-total=\"1\""));
    assert!(tag_index.contains("data-prev=\"\""));
    assert!(tag_index.contains("data-next=\"\""));

    assert!(!root.join("html/tags/shared/gamma").exists());
    assert!(!root.join("html/tags/shared/beta").exists());
    assert!(!root.join("html/tags/shared/alpha").exists());
}

#[test]
fn generates_rss_feed_with_absolute_urls() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);
    fs::write(
        root.join("bckt.yaml"),
        "base_url: \"https://example.com/blog\"\n",
    )
    .unwrap();

    write_dated_post(root, "alpha", "2024-01-01T00:00:00Z", "Alpha body");
    write_dated_post(root, "beta", "2024-02-01T00:00:00Z", "Beta body");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let feed = fs::read_to_string(root.join("html/rss.xml")).unwrap();
    assert!(feed.contains("<link>https://example.com/blog/</link>"));
    assert!(feed.contains("<atom:link href=\"https://example.com/blog/rss.xml\""));
    assert!(feed.contains("<link>https://example.com/blog/2024/02/01/beta/</link>"));
    assert!(feed.contains("<description>Beta body"));
    assert!(feed.contains("<content:encoded><![CDATA["));
}

#[test]
fn generates_tag_rss_feeds_when_configured() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);
    fs::write(
        root.join("bckt.yaml"),
        "title: Demo Site\nbase_url: \"https://example.com\"\nrss_tags:\n  - shared\n",
    )
    .unwrap();

    write_tagged_post(root, "alpha", "shared", "2024-01-01T00:00:00Z", "A");
    write_tagged_post(root, "beta", "other", "2024-02-01T00:00:00Z", "B");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let feed_path = root.join("html/rss-shared.xml");
    assert!(feed_path.exists());
    let feed = fs::read_to_string(feed_path).unwrap();
    assert!(feed.contains("shared · Demo Site"));
    assert!(feed.contains("/2024/01/01/alpha/"));
    assert!(!feed.contains("/2024/02/01/beta/"));
}

#[test]
fn keeps_relative_paths_in_html_and_absolute_in_feeds() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts/media/images")).unwrap();
    setup_markdown_templates(root);
    fs::write(root.join("posts/media/images/pic.png"), "image-bytes").unwrap();
    fs::write(root.join("posts/media/notes.txt"), "notes").unwrap();
    fs::write(
            root.join("posts/media/post.md"),
            "---\ndate: 2024-01-01T00:00:00Z\nattached:\n  - images/pic.png\n  - notes.txt\n---\n![Alt](images/pic.png)\n\n[Download](notes.txt)\n",
        )
        .unwrap();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let post_page = fs::read_to_string(root.join("html/2024/01/01/media/index.html")).unwrap();
    // HTML pages use relative paths (works regardless of base_url)
    assert!(post_page.contains("images/pic.png"));
    assert!(post_page.contains("notes.txt"));
    // Should not contain absolute paths
    assert!(!post_page.contains("/2024/01/01/media/images/pic.png"));
    assert!(!post_page.contains("/2024/01/01/media/notes.txt"));

    let feed = fs::read_to_string(root.join("html/rss.xml")).unwrap();
    // RSS feeds use absolute URLs (required for feed readers)
    assert!(feed.contains("/2024/01/01/media/images/pic.png"));
    assert!(feed.contains("/2024/01/01/media/notes.txt"));
}

#[test]
fn generates_sitemap_with_posts_tags_and_pages() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);
    fs::write(
        root.join("bckt.yaml"),
        "base_url: \"https://example.com/blog\"\nhomepage_posts: 1\npaginate_tags: true\n",
    )
    .unwrap();

    write_tagged_post(root, "alpha", "shared", "2024-01-01T00:00:00Z", "A");
    write_tagged_post(root, "beta", "shared", "2024-02-01T00:00:00Z", "B");
    write_tagged_post(root, "gamma", "shared", "2024-03-01T00:00:00Z", "C");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let sitemap = fs::read_to_string(root.join("html/sitemap.xml")).unwrap();
    assert!(sitemap.contains("<loc>https://example.com/blog/</loc>"));

    // Page-number based URLs (page 1 = oldest, page 2 = middle)
    assert!(sitemap.contains("<loc>https://example.com/blog/page/1/</loc>"));
    assert!(sitemap.contains("<loc>https://example.com/blog/page/2/</loc>"));
    assert!(sitemap.contains("<loc>https://example.com/blog/tags/shared/</loc>"));
    assert!(sitemap.contains("<loc>https://example.com/blog/2024/03/01/gamma/</loc>"));
    assert!(sitemap.contains("<lastmod>2024-03-01T00:00:00Z</lastmod>"));
}

#[test]
fn skips_rewriting_tag_index_when_unchanged() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_tagged_post(root, "alpha", "shared", "2024-01-01T00:00:00Z", "A");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let tag_path = root.join("html/tags/shared/index.html");
    assert!(tag_path.exists());
    let first_mtime = file_mtime(&tag_path);

    wait_for_filesystem_tick();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Changed,
            verbose: false,
        },
    )
    .unwrap();

    let second_mtime = file_mtime(&tag_path);
    assert_eq!(first_mtime, second_mtime);
}

#[test]
fn rerenders_tag_index_when_post_changes() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_tagged_post(root, "alpha", "shared", "2024-01-01T00:00:00Z", "A");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let tag_path = root.join("html/tags/shared/index.html");
    let first_mtime = file_mtime(&tag_path);

    wait_for_filesystem_tick();

    fs::write(
            root.join("posts/alpha/post.md"),
            "---\ntitle: Alpha Updated\ndate: 2024-01-01T00:00:00Z\nslug: alpha\ntags:\n  - shared\n---\nUpdated",
        )
        .unwrap();

    wait_for_filesystem_tick();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Changed,
            verbose: false,
        },
    )
    .unwrap();

    let second_mtime = file_mtime(&tag_path);
    assert!(second_mtime > first_mtime);
}

#[test]
fn removes_tag_index_when_tag_disappears() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_tagged_post(root, "alpha", "shared", "2024-01-01T00:00:00Z", "A");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let tag_path = root.join("html/tags/shared/index.html");
    assert!(tag_path.exists());

    wait_for_filesystem_tick();

    fs::remove_dir_all(root.join("posts/alpha")).unwrap();

    wait_for_filesystem_tick();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Changed,
            verbose: false,
        },
    )
    .unwrap();

    assert!(!tag_path.exists());
}

#[test]
fn skips_rewriting_archives_when_unchanged() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_dated_post(root, "alpha", "2024-02-01T00:00:00Z", "A");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let year_path = root.join("html/2024/index.html");
    let month_path = root.join("html/2024/02/index.html");
    let first_year_mtime = file_mtime(&year_path);
    let first_month_mtime = file_mtime(&month_path);

    wait_for_filesystem_tick();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Changed,
            verbose: false,
        },
    )
    .unwrap();

    let second_year_mtime = file_mtime(&year_path);
    let second_month_mtime = file_mtime(&month_path);

    assert_eq!(first_year_mtime, second_year_mtime);
    assert_eq!(first_month_mtime, second_month_mtime);
}

#[test]
fn rerenders_archives_when_post_changes() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_dated_post(root, "alpha", "2024-03-01T00:00:00Z", "Original");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let year_path = root.join("html/2024/index.html");
    let month_path = root.join("html/2024/03/index.html");
    let first_year_mtime = file_mtime(&year_path);
    let first_month_mtime = file_mtime(&month_path);

    wait_for_filesystem_tick();

    fs::write(
            root.join("posts/alpha/post.md"),
            "---\ntitle: Alpha\ndate: 2024-03-01T00:00:00Z\nslug: alpha\ntags:\n  - alpha\n---\nUpdated body",
        )
        .unwrap();

    wait_for_filesystem_tick();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Changed,
            verbose: false,
        },
    )
    .unwrap();

    let second_year_mtime = file_mtime(&year_path);
    let second_month_mtime = file_mtime(&month_path);

    assert!(second_year_mtime > first_year_mtime);
    assert!(second_month_mtime > first_month_mtime);
}

#[test]
fn removes_archives_when_posts_are_removed() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_dated_post(root, "alpha", "2024-04-01T00:00:00Z", "Body");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    let year_path = root.join("html/2024/index.html");
    let month_path = root.join("html/2024/04/index.html");
    assert!(year_path.exists());
    assert!(month_path.exists());

    wait_for_filesystem_tick();

    fs::remove_dir_all(root.join("posts/alpha")).unwrap();

    wait_for_filesystem_tick();

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Changed,
            verbose: false,
        },
    )
    .unwrap();

    assert!(!year_path.exists());
    assert!(!month_path.exists());
}

#[test]
fn renders_year_and_month_archives() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    fs::create_dir_all(root.join("posts")).unwrap();
    setup_markdown_templates(root);

    write_dated_post(root, "jan", "2023-01-01T00:00:00Z", "Old");
    write_dated_post(root, "feb", "2024-02-01T00:00:00Z", "Mid");
    write_dated_post(root, "mar", "2024-03-01T00:00:00Z", "New");

    render_site(
        root,
        RenderPlan {
            posts: true,
            static_assets: false,
            mode: BuildMode::Full,
            verbose: false,
        },
    )
    .unwrap();

    assert!(root.join("html/2024/index.html").exists());
    assert!(root.join("html/2024/03/index.html").exists());
    assert!(root.join("html/2023/index.html").exists());
}

#[test]
fn incremental_rebuilds_only_changed_post() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    setup_markdown_templates(root);

    write_dated_post(root, "alpha", "2024-01-01T00:00:00Z", "Alpha body");
    write_dated_post(root, "beta", "2024-02-01T00:00:00Z", "Beta body");

    let alpha_output = root.join("html/2024/01/01/alpha/index.html");
    let beta_output = root.join("html/2024/02/01/beta/index.html");

    let full_plan = RenderPlan {
        posts: true,
        static_assets: false,
        mode: BuildMode::Full,
        verbose: false,
    };
    let changed_plan = RenderPlan {
        posts: true,
        static_assets: false,
        mode: BuildMode::Changed,
        verbose: false,
    };

    render_site(root, full_plan).unwrap();

    let alpha_first = file_mtime(&alpha_output);
    let beta_first = file_mtime(&beta_output);

    wait_for_filesystem_tick();
    render_site(root, changed_plan).unwrap();

    let alpha_second = file_mtime(&alpha_output);
    let beta_second = file_mtime(&beta_output);
    assert_eq!(alpha_first, alpha_second);
    assert_eq!(beta_first, beta_second);

    wait_for_filesystem_tick();
    write_dated_post(root, "alpha", "2024-01-01T00:00:00Z", "Alpha updated");
    render_site(root, changed_plan).unwrap();

    let alpha_third = file_mtime(&alpha_output);
    let beta_third = file_mtime(&beta_output);
    assert!(alpha_third > alpha_second);
    assert_eq!(beta_second, beta_third);
}

#[test]
fn template_change_triggers_full_rebuild() {
    let temp = TempDir::new().unwrap();
    let root = temp.path();
    setup_markdown_templates(root);

    write_dated_post(root, "alpha", "2024-01-01T00:00:00Z", "Alpha body");
    write_dated_post(root, "beta", "2024-02-01T00:00:00Z", "Beta body");

    let alpha_output = root.join("html/2024/01/01/alpha/index.html");
    let beta_output = root.join("html/2024/02/01/beta/index.html");

    let full_plan = RenderPlan {
        posts: true,
        static_assets: false,
        mode: BuildMode::Full,
        verbose: false,
    };
    let changed_plan = RenderPlan {
        posts: true,
        static_assets: false,
        mode: BuildMode::Changed,
        verbose: false,
    };

    render_site(root, full_plan).unwrap();
    let alpha_initial = file_mtime(&alpha_output);
    let beta_initial = file_mtime(&beta_output);

    wait_for_filesystem_tick();
    render_site(root, changed_plan).unwrap();
    let alpha_after_changed = file_mtime(&alpha_output);
    let beta_after_changed = file_mtime(&beta_output);
    assert_eq!(alpha_initial, alpha_after_changed);
    assert_eq!(beta_initial, beta_after_changed);

    wait_for_filesystem_tick();
    write_template(
        root,
        "base.html",
        "<!doctype html><html><body data-version=\"v2\">{% block content %}{% endblock %}</body></html>",
    );
    render_site(root, changed_plan).unwrap();

    let alpha_after_template = file_mtime(&alpha_output);
    let beta_after_template = file_mtime(&beta_output);
    assert!(alpha_after_template > alpha_after_changed);
    assert!(beta_after_template > beta_after_changed);
}
