// Copyright © 2023 - 2026 Static Site Generator (SSG). All rights reserved.
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Project scaffolding.
//!
//! Generates a complete starter project structure when `ssg --new`
//! is invoked, including content, templates, config, and static assets.

use anyhow::{Context, Result};
use std::{fs, path::Path};

/// Writes a file inside the scaffold project, with a contextual error message.
fn write_scaffold_file(
    path: &Path,
    content: impl AsRef<[u8]>,
    label: &str,
) -> Result<()> {
    fs::write(path, content).with_context(|| format!("Failed to write {label}"))
}

/// Generates a new project with the given name in the current directory.
pub fn scaffold_project(name: &str) -> Result<()> {
    let cwd = std::env::current_dir()?;
    scaffold_project_at(name, &cwd)
}

/// Generates a new project at the given base directory.
pub fn scaffold_project_at(name: &str, base: &Path) -> Result<()> {
    let root = base.join(name);
    if root.exists() {
        anyhow::bail!("Directory '{name}' already exists");
    }

    create_scaffold_dirs(name, &root)?;
    write_config_file(name, &root)?;
    write_content_files(name, &root)?;
    write_template_files(&root)?;
    write_static_assets(&root)?;
    write_data_files(&root)?;

    println!("Created new project: {name}");
    println!("  cd {name}");
    println!("  ssg -f config.toml");

    Ok(())
}

/// Creates the scaffold directory structure.
fn create_scaffold_dirs(name: &str, root: &Path) -> Result<()> {
    let dirs = [
        "",
        "content",
        "content/blog",
        "templates/tera",
        "static/css",
        "data",
    ];
    for dir in &dirs {
        fail_point!("scaffold::create-dir", |_| {
            anyhow::bail!("injected: scaffold::create-dir")
        });
        fs::create_dir_all(root.join(dir))
            .with_context(|| format!("Failed to create {name}/{dir}"))?;
    }
    Ok(())
}

/// Writes the config.toml file.
fn write_config_file(name: &str, root: &Path) -> Result<()> {
    fail_point!("scaffold::write-config", |_| {
        anyhow::bail!("injected: scaffold::write-config")
    });
    write_scaffold_file(
        &root.join("config.toml"),
        format!(
            r#"site_name = "{name}"
content_dir = "content"
output_dir = "public"
template_dir = "templates"
base_url = "http://127.0.0.1:8000"
site_title = "{name}"
site_description = "A site built with SSG"
language = "en-GB"
"#
        ),
        "config.toml",
    )
}

/// Writes all content markdown files.
fn write_content_files(name: &str, root: &Path) -> Result<()> {
    fail_point!("scaffold::write-index", |_| {
        anyhow::bail!("injected: scaffold::write-index")
    });
    write_scaffold_file(
        &root.join("content/index.md"),
        format!(
            r"---
title: Welcome to {name}
description: A fast, accessible static site built with SSG
layout: index
---

# Welcome

This is your new SSG site. Edit `content/index.md` to get started.

## Features

- Tera templating with inheritance
- WCAG 2.1 AA accessibility by default
- JSON-LD structured data for SEO
- Syntax highlighting for code blocks
- Responsive image optimisation
- Client-side search
"
        ),
        "content/index.md",
    )?;

    fail_point!("scaffold::write-about", |_| {
        anyhow::bail!("injected: scaffold::write-about")
    });
    write_scaffold_file(
        &root.join("content/about.md"),
        r"---
title: About
description: About this site
layout: page
---

# About

This page was generated by [SSG](https://static-site-generator.one).
",
        "content/about.md",
    )?;

    fail_point!("scaffold::write-post", |_| {
        anyhow::bail!("injected: scaffold::write-post")
    });
    write_scaffold_file(
        &root.join("content/blog/first-post.md"),
        format!(
            r#"---
title: First Post
description: My first blog post
layout: post
date: 2026-01-01
author: {name} Team
tags:
  - welcome
  - getting-started
categories:
  - blog
---

# First Post

Welcome to **{name}**! This is your first blog post.

## Code Example

```rust
fn main() {{
    println!("Hello from {name}!");
}}
```

{{{{< tip >}}}}
Edit this file at `content/blog/first-post.md`.
{{{{< /tip >}}}}
"#
        ),
        "content/blog/first-post.md",
    )
}

/// Writes all template files.
fn write_template_files(root: &Path) -> Result<()> {
    fail_point!("scaffold::write-base", |_| {
        anyhow::bail!("injected: scaffold::write-base")
    });
    write_scaffold_file(
        &root.join("templates/tera/base.html"),
        r##"<!DOCTYPE html>
<html lang="{{ site.language | default(value='en') }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block title %}{{ page.title | default(value="Untitled") }}{% if site.title %} — {{ site.title }}{% endif %}{% endblock %}</title>
  {% if page.description %}<meta name="description" content="{{ page.description }}">{% endif %}
  <link rel="stylesheet" href="/css/style.css">
  {% block head_extra %}{% endblock %}
</head>
<body>
  <a href="#main-content" class="sr-only">Skip to main content</a>
  <header role="banner">
    <nav aria-label="Main navigation">
      <a href="/">{{ site.name | default(value="Home") }}</a>
      <a href="/about.html">About</a>
    </nav>
  </header>
  <main id="main-content" role="main">
    {% block content %}{% endblock %}
  </main>
  <footer role="contentinfo">
    <p>&copy; {{ site.name | default(value="") }}. Built with <a href="https://static-site-generator.one">SSG</a>.</p>
  </footer>
</body>
</html>
"##,
        "templates/tera/base.html",
    )?;

    fail_point!("scaffold::write-page-tpl", |_| {
        anyhow::bail!("injected: scaffold::write-page-tpl")
    });
    write_scaffold_file(
        &root.join("templates/tera/page.html"),
        r#"{% extends "base.html" %}
{% block content %}{{ page.content | safe }}{% endblock %}
"#,
        "templates/tera/page.html",
    )?;

    fail_point!("scaffold::write-post-tpl", |_| {
        anyhow::bail!("injected: scaffold::write-post-tpl")
    });
    write_scaffold_file(
        &root.join("templates/tera/post.html"),
        r#"{% extends "base.html" %}
{% block content %}
<article>
  <header>
    <h1>{{ page.title | default(value="") }}</h1>
    {% if page.date %}<time datetime="{{ page.date }}">{{ page.date }}</time>{% endif %}
    {% if page.author %}<span class="author">by {{ page.author }}</span>{% endif %}
    {% if page.content %}<span class="reading-time">{{ page.content | reading_time }}</span>{% endif %}
  </header>
  <div class="post-body">{{ page.content | safe }}</div>
  {% if page.tags %}
  <footer>
    <ul class="tags" aria-label="Tags">
      {% for tag in page.tags %}<li><a href="/tags/{{ tag | slugify }}/">{{ tag }}</a></li>{% endfor %}
    </ul>
  </footer>
  {% endif %}
</article>
{% endblock %}
"#,
        "templates/tera/post.html",
    )?;

    fail_point!("scaffold::write-index-tpl", |_| {
        anyhow::bail!("injected: scaffold::write-index-tpl")
    });
    write_scaffold_file(
        &root.join("templates/tera/index.html"),
        r#"{% extends "base.html" %}
{% block title %}{{ site.title | default(value="Home") }}{% endblock %}
{% block content %}
<section>{{ page.content | safe }}</section>
{% endblock %}
"#,
        "templates/tera/index.html",
    )
}

/// Writes static assets (CSS).
fn write_static_assets(root: &Path) -> Result<()> {
    fail_point!("scaffold::write-css", |_| {
        anyhow::bail!("injected: scaffold::write-css")
    });
    write_scaffold_file(
        &root.join("static/css/style.css"),
        r#"/* SSG Default Styles */
:root {
  --text: #1a1a2e;
  --bg: #ffffff;
  --accent: #0066cc;
  --muted: #6b7280;
  --border: #e5e7eb;
  --radius: 6px;
}
@media (prefers-color-scheme: dark) {
  :root {
    --text: #e6edf3;
    --bg: #0d1117;
    --accent: #58a6ff;
    --muted: #8b949e;
    --border: #30363d;
  }
}
*, *::before, *::after { box-sizing: border-box; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: var(--text);
  background: var(--bg);
  line-height: 1.6;
  max-width: 48rem;
  margin: 0 auto;
  padding: 1rem 1.5rem;
}
a { color: var(--accent); }
nav { display: flex; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid var(--border); }
main { padding: 2rem 0; }
footer { border-top: 1px solid var(--border); padding: 1rem 0; color: var(--muted); font-size: 0.875rem; }
pre { background: #f6f8fa; border: 1px solid var(--border); border-radius: var(--radius); padding: 1em; overflow-x: auto; }
@media (prefers-color-scheme: dark) { pre { background: #161b22; } }
code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 0.875em; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
.admonition { border-left: 4px solid var(--accent); padding: 0.75rem 1rem; margin: 1rem 0; border-radius: var(--radius); }
.admonition-title { font-weight: 600; margin-bottom: 0.25rem; }
.tags { list-style: none; padding: 0; display: flex; gap: 0.5rem; }
.tags li a { background: var(--border); padding: 0.25rem 0.5rem; border-radius: var(--radius); text-decoration: none; font-size: 0.875rem; }
article time, article .author, article .reading-time { color: var(--muted); font-size: 0.875rem; margin-right: 1rem; }
"#,
        "static/css/style.css",
    )
}

/// Writes data files (nav.toml).
fn write_data_files(root: &Path) -> Result<()> {
    fail_point!("scaffold::write-nav", |_| {
        anyhow::bail!("injected: scaffold::write-nav")
    });
    write_scaffold_file(
        &root.join("data/nav.toml"),
        r#"[[links]]
name = "Home"
url = "/"

[[links]]
name = "About"
url = "/about.html"

[[links]]
name = "Blog"
url = "/blog/"
"#,
        "data/nav.toml",
    )
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    // -------------------------------------------------------------------
    // scaffold_project_at — directory layout
    // -------------------------------------------------------------------

    #[test]
    fn scaffold_project_at_creates_complete_directory_structure() {
        let dir = tempdir().unwrap();
        let name = "test-site";
        let project = dir.path().join(name);
        scaffold_project_at(name, dir.path()).unwrap();

        // Every directory in the `dirs` array must exist.
        for sub in [
            "",
            "content",
            "content/blog",
            "templates/tera",
            "static/css",
            "data",
        ] {
            assert!(
                project.join(sub).exists(),
                "subdirectory `{sub}` should have been created"
            );
        }
    }

    #[test]
    fn scaffold_project_at_writes_all_expected_files() {
        let dir = tempdir().unwrap();
        let name = "demo";
        let project = dir.path().join(name);
        scaffold_project_at(name, dir.path()).unwrap();

        for file in [
            "config.toml",
            "content/index.md",
            "content/about.md",
            "content/blog/first-post.md",
            "templates/tera/base.html",
            "templates/tera/page.html",
            "templates/tera/post.html",
            "templates/tera/index.html",
            "static/css/style.css",
            "data/nav.toml",
        ] {
            assert!(
                project.join(file).exists(),
                "file `{file}` should have been scaffolded"
            );
        }
    }

    // -------------------------------------------------------------------
    // scaffold_project_at — content-template personalisation
    // -------------------------------------------------------------------

    #[test]
    fn scaffold_project_at_injects_project_name_into_config() {
        let dir = tempdir().unwrap();
        let name = "my-cool-site";
        scaffold_project_at(name, dir.path()).unwrap();

        let config =
            fs::read_to_string(dir.path().join(name).join("config.toml"))
                .unwrap();
        assert!(config.contains(&format!(r#"site_name = "{name}""#)));
        assert!(config.contains(&format!(r#"site_title = "{name}""#)));
        assert!(config.contains(r#"language = "en-GB""#));
    }

    #[test]
    fn scaffold_project_at_injects_project_name_into_index_md() {
        let dir = tempdir().unwrap();
        let name = "hello";
        scaffold_project_at(name, dir.path()).unwrap();

        let index =
            fs::read_to_string(dir.path().join(name).join("content/index.md"))
                .unwrap();
        assert!(index.contains(&format!("title: Welcome to {name}")));
        assert!(index.contains("layout: index"));
    }

    #[test]
    fn scaffold_project_at_injects_project_name_into_first_post() {
        let dir = tempdir().unwrap();
        let name = "projectx";
        scaffold_project_at(name, dir.path()).unwrap();

        let post = fs::read_to_string(
            dir.path().join(name).join("content/blog/first-post.md"),
        )
        .unwrap();
        assert!(post.contains(&format!("author: {name} Team")));
        assert!(post.contains(&format!("Welcome to **{name}**")));
        assert!(post.contains(&format!(r#"println!("Hello from {name}!");"#)));
    }

    #[test]
    fn scaffold_project_at_static_assets_include_dark_mode_block() {
        // Guards the prefers-color-scheme media query in style.css —
        // accessibility regression tripwire.
        let dir = tempdir().unwrap();
        scaffold_project_at("a", dir.path()).unwrap();
        let css = fs::read_to_string(
            dir.path().join("a").join("static/css/style.css"),
        )
        .unwrap();
        assert!(css.contains("@media (prefers-color-scheme: dark)"));
        assert!(css.contains(".sr-only"));
    }

    #[test]
    fn scaffold_project_at_base_template_has_accessibility_landmarks() {
        let dir = tempdir().unwrap();
        scaffold_project_at("x", dir.path()).unwrap();
        let base = fs::read_to_string(
            dir.path().join("x").join("templates/tera/base.html"),
        )
        .unwrap();
        assert!(base.contains(r#"role="banner""#));
        assert!(base.contains(r#"role="main""#));
        assert!(base.contains(r#"role="contentinfo""#));
        assert!(base.contains(r#"aria-label="Main navigation""#));
        assert!(base.contains(r#"class="sr-only""#));
    }

    #[test]
    fn scaffold_project_at_nav_toml_has_three_default_links() {
        let dir = tempdir().unwrap();
        scaffold_project_at("y", dir.path()).unwrap();
        let nav =
            fs::read_to_string(dir.path().join("y").join("data/nav.toml"))
                .unwrap();
        assert_eq!(nav.matches("[[links]]").count(), 3);
        assert!(nav.contains(r#"name = "Home""#));
        assert!(nav.contains(r#"name = "About""#));
        assert!(nav.contains(r#"name = "Blog""#));
    }

    // -------------------------------------------------------------------
    // scaffold_project_at — failure paths
    // -------------------------------------------------------------------

    #[test]
    fn scaffold_project_at_refuses_to_overwrite_existing_directory() {
        // The `anyhow::bail!` at line 22 protects user content from
        // being silently overwritten.
        let dir = tempdir().unwrap();
        let name = "existing";
        fs::create_dir(dir.path().join(name)).unwrap();

        let err = scaffold_project_at(name, dir.path()).unwrap_err();
        let msg = format!("{err}");
        assert!(
            msg.contains("already exists"),
            "error should mention `already exists`: {msg}"
        );
    }

    #[test]
    fn scaffold_project_at_refuses_to_overwrite_existing_file() {
        // Same guard, but the pre-existing entry is a file, not a
        // directory — both trigger the `root.exists()` check.
        let dir = tempdir().unwrap();
        let name = "blocker";
        fs::write(dir.path().join(name), "i exist").unwrap();

        assert!(scaffold_project_at(name, dir.path()).is_err());
    }

    // -------------------------------------------------------------------
    // scaffold_project — default wrapper around CWD
    // -------------------------------------------------------------------

    #[test]
    fn scaffold_project_uses_current_working_directory() {
        // Exercises the `scaffold_project` entry point at line 13,
        // which wraps `scaffold_project_at` with env::current_dir().
        // We pushd into a tempdir so we don't pollute the repo root.
        let dir = tempdir().unwrap();
        let prev = std::env::current_dir().expect("read current dir");
        std::env::set_current_dir(&dir).expect("pushd");

        let result = scaffold_project("from-cwd");

        // Always restore cwd, even if the call failed.
        std::env::set_current_dir(&prev).expect("popd");

        result.expect("scaffold should succeed in a fresh cwd");
        assert!(dir.path().join("from-cwd").join("config.toml").exists());
    }

    // -----------------------------------------------------------------
    // write_scaffold_file — unit tests
    // -----------------------------------------------------------------

    #[test]
    fn write_scaffold_file_creates_file() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        write_scaffold_file(&path, "hello", "test.txt").unwrap();
        assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
    }

    #[test]
    fn write_scaffold_file_overwrites_existing() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.txt");
        fs::write(&path, "old").unwrap();
        write_scaffold_file(&path, "new", "test.txt").unwrap();
        assert_eq!(fs::read_to_string(&path).unwrap(), "new");
    }

    #[test]
    fn write_scaffold_file_error_has_label() {
        let err = write_scaffold_file(
            Path::new("/no/such/dir/file.txt"),
            "x",
            "my-label",
        )
        .unwrap_err();
        let msg = format!("{err:#}");
        assert!(
            msg.contains("my-label"),
            "error should include the label: {msg}"
        );
    }

    #[test]
    fn write_scaffold_file_empty_content() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("empty.txt");
        write_scaffold_file(&path, "", "empty.txt").unwrap();
        assert_eq!(fs::read_to_string(&path).unwrap(), "");
    }

    #[test]
    fn write_scaffold_file_binary_content() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("bin.dat");
        let data: &[u8] = &[0x00, 0xFF, 0xAB, 0xCD];
        write_scaffold_file(&path, data, "bin.dat").unwrap();
        assert_eq!(fs::read(&path).unwrap(), data);
    }

    // -----------------------------------------------------------------
    // create_scaffold_dirs — direct tests
    // -----------------------------------------------------------------

    #[test]
    fn create_scaffold_dirs_creates_all_expected_dirs() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        create_scaffold_dirs("proj", &root).unwrap();

        for sub in [
            "",
            "content",
            "content/blog",
            "templates/tera",
            "static/css",
            "data",
        ] {
            assert!(root.join(sub).exists(), "{sub} should exist");
        }
    }

    #[test]
    fn create_scaffold_dirs_idempotent() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        create_scaffold_dirs("proj", &root).unwrap();
        // Calling again should not fail
        create_scaffold_dirs("proj", &root).unwrap();
    }

    // -----------------------------------------------------------------
    // write_config_file — direct tests
    // -----------------------------------------------------------------

    #[test]
    fn write_config_file_content() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(&root).unwrap();
        write_config_file("my-site", &root).unwrap();

        let content = fs::read_to_string(root.join("config.toml")).unwrap();
        assert!(content.contains(r#"site_name = "my-site""#));
        assert!(content.contains(r#"site_title = "my-site""#));
        assert!(content.contains(r#"content_dir = "content""#));
        assert!(content.contains(r#"output_dir = "public""#));
        assert!(content.contains(r#"template_dir = "templates""#));
        assert!(content.contains("http://127.0.0.1:8000"));
        assert!(content.contains(r#"language = "en-GB""#));
    }

    // -----------------------------------------------------------------
    // write_content_files — direct tests
    // -----------------------------------------------------------------

    #[test]
    fn write_content_files_creates_all_content() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("content/blog")).unwrap();
        write_content_files("test-proj", &root).unwrap();

        assert!(root.join("content/index.md").exists());
        assert!(root.join("content/about.md").exists());
        assert!(root.join("content/blog/first-post.md").exists());
    }

    #[test]
    fn write_content_files_about_has_correct_frontmatter() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("content/blog")).unwrap();
        write_content_files("test-proj", &root).unwrap();

        let about = fs::read_to_string(root.join("content/about.md")).unwrap();
        assert!(about.contains("title: About"));
        assert!(about.contains("layout: page"));
        assert!(about.contains("static-site-generator.one"));
    }

    #[test]
    fn write_content_files_index_has_features_list() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("content/blog")).unwrap();
        write_content_files("proj", &root).unwrap();

        let index = fs::read_to_string(root.join("content/index.md")).unwrap();
        assert!(index.contains("## Features"));
        assert!(index.contains("- Tera templating"));
    }

    #[test]
    fn write_content_files_first_post_has_tags_and_code() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("content/blog")).unwrap();
        write_content_files("proj", &root).unwrap();

        let post = fs::read_to_string(root.join("content/blog/first-post.md"))
            .unwrap();
        assert!(post.contains("tags:"));
        assert!(post.contains("- welcome"));
        assert!(post.contains("```rust"));
        assert!(post.contains("categories:"));
    }

    // -----------------------------------------------------------------
    // write_template_files — direct tests
    // -----------------------------------------------------------------

    #[test]
    fn write_template_files_creates_all_templates() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("templates/tera")).unwrap();
        write_template_files(&root).unwrap();

        for file in [
            "templates/tera/base.html",
            "templates/tera/page.html",
            "templates/tera/post.html",
            "templates/tera/index.html",
        ] {
            assert!(root.join(file).exists(), "{file} should exist");
        }
    }

    #[test]
    fn write_template_files_page_extends_base() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("templates/tera")).unwrap();
        write_template_files(&root).unwrap();

        let page =
            fs::read_to_string(root.join("templates/tera/page.html")).unwrap();
        assert!(page.contains(r#"extends "base.html""#));
        assert!(page.contains("block content"));
    }

    #[test]
    fn write_template_files_post_has_article_structure() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("templates/tera")).unwrap();
        write_template_files(&root).unwrap();

        let post =
            fs::read_to_string(root.join("templates/tera/post.html")).unwrap();
        assert!(post.contains("<article>"));
        assert!(post.contains("page.title"));
        assert!(post.contains("page.date"));
        assert!(post.contains("page.tags"));
        assert!(post.contains("reading_time"));
    }

    #[test]
    fn write_template_files_index_extends_base() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("templates/tera")).unwrap();
        write_template_files(&root).unwrap();

        let index =
            fs::read_to_string(root.join("templates/tera/index.html")).unwrap();
        assert!(index.contains(r#"extends "base.html""#));
        assert!(index.contains("site.title"));
    }

    // -----------------------------------------------------------------
    // write_static_assets — direct tests
    // -----------------------------------------------------------------

    #[test]
    fn write_static_assets_creates_stylesheet() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("static/css")).unwrap();
        write_static_assets(&root).unwrap();

        let css =
            fs::read_to_string(root.join("static/css/style.css")).unwrap();
        assert!(css.contains(":root"));
        assert!(css.contains("--text:"));
        assert!(css.contains("--bg:"));
        assert!(css.contains("--accent:"));
        assert!(css.contains("box-sizing: border-box"));
    }

    // -----------------------------------------------------------------
    // write_data_files — direct tests
    // -----------------------------------------------------------------

    #[test]
    fn write_data_files_creates_nav_toml() {
        let dir = tempdir().unwrap();
        let root = dir.path().join("proj");
        fs::create_dir_all(root.join("data")).unwrap();
        write_data_files(&root).unwrap();

        let nav = fs::read_to_string(root.join("data/nav.toml")).unwrap();
        assert_eq!(nav.matches("[[links]]").count(), 3);
        assert!(nav.contains(r#"url = "/""#));
        assert!(nav.contains(r#"url = "/about.html""#));
        assert!(nav.contains(r#"url = "/blog/""#));
    }

    // -----------------------------------------------------------------
    // scaffold_project_at — name injection edge cases
    // -----------------------------------------------------------------

    #[test]
    fn scaffold_project_at_special_chars_in_name() {
        let dir = tempdir().unwrap();
        let name = "my-cool_site.2026";
        scaffold_project_at(name, dir.path()).unwrap();

        let config =
            fs::read_to_string(dir.path().join(name).join("config.toml"))
                .unwrap();
        assert!(config.contains(&format!(r#"site_name = "{name}""#)));
    }

    #[test]
    fn scaffold_project_at_single_char_name() {
        let dir = tempdir().unwrap();
        scaffold_project_at("z", dir.path()).unwrap();
        assert!(dir.path().join("z").join("config.toml").exists());
    }

    // -----------------------------------------------------------------
    // scaffold_project_at — template content validation
    // -----------------------------------------------------------------

    #[test]
    fn scaffold_project_at_base_template_is_valid_html() {
        let dir = tempdir().unwrap();
        scaffold_project_at("t", dir.path()).unwrap();
        let base = fs::read_to_string(
            dir.path().join("t").join("templates/tera/base.html"),
        )
        .unwrap();
        assert!(base.starts_with("<!DOCTYPE html>"));
        assert!(base.contains("</html>"));
        assert!(base.contains("<head>"));
        assert!(base.contains("</head>"));
        assert!(base.contains("<body>"));
        assert!(base.contains("</body>"));
    }

    #[test]
    fn scaffold_project_at_config_has_all_required_keys() {
        let dir = tempdir().unwrap();
        scaffold_project_at("k", dir.path()).unwrap();
        let config =
            fs::read_to_string(dir.path().join("k").join("config.toml"))
                .unwrap();
        for key in [
            "site_name",
            "content_dir",
            "output_dir",
            "template_dir",
            "base_url",
            "site_title",
            "site_description",
            "language",
        ] {
            assert!(
                config.contains(key),
                "config.toml should contain key: {key}"
            );
        }
    }
}
