"""Tests for exokit.core.scaffold — rendering engine.

These tests create temporary Jinja2 templates so they can run independently
of the real templates (which may be created in parallel by the template-dev
agent).
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from jinja2 import UndefinedError

from exokit.core.models import DemoManifest, ScaffoldContext, ScaffoldResult
from exokit.core.scaffold import (
    _STUB_DIRECTORIES,
    INDUSTRY_DISPLAY_NAMES,
    _build_jinja_env,
    _create_directories,
    _map_template_path,
    _slugify,
    _snake_case,
    _title_case,
    generate,
)

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def templates_dir(tmp_path: Path) -> Path:
    """Create a minimal set of test templates in a temp directory."""
    tpl_root = tmp_path / "templates"

    # root/ templates
    root = tpl_root / "root"
    root.mkdir(parents=True)
    (root / "CLAUDE.md.j2").write_text(
        "# {{ company }} Demo\nBundle: {{ bundle_name }}\n",
        encoding="utf-8",
    )
    (root / ".env.example.j2").write_text(
        "COMPANY={{ company }}\nINDUSTRY={{ industry }}\n",
        encoding="utf-8",
    )

    # root/ templates (databricks.yml must be at project root)
    (root / "databricks.yml.j2").write_text(
        "bundle:\n  name: {{ bundle_name }}\n  host: {{ workspace_host }}\n",
        encoding="utf-8",
    )

    # docs/ templates
    docs = tpl_root / "docs"
    docs.mkdir(parents=True)
    (docs / "PROGRESS.md.j2").write_text(
        "# Progress for {{ company }}\n",
        encoding="utf-8",
    )

    # scripts/ templates
    scripts = tpl_root / "scripts"
    scripts.mkdir(parents=True)
    (scripts / "deploy.sh.j2").write_text(
        "#!/bin/bash\n# Deploy {{ bundle_name }}\necho 'deploying'\n",
        encoding="utf-8",
    )

    # claude/ templates (maps to .claude/)
    claude = tpl_root / "claude"
    claude_agents = claude / "agents"
    claude_agents.mkdir(parents=True)
    (claude_agents / "lakehouse-dev.md.j2").write_text(
        "# Lakehouse Dev Agent for {{ company }}\nIndustry: {{ industry }}\n",
        encoding="utf-8",
    )

    # Template using custom filters
    (root / "slugified.txt.j2").write_text(
        "{{ company | slugify }}\n{{ company | snake_case }}\n{{ company | title_case }}\n",
        encoding="utf-8",
    )

    return tpl_root


@pytest.fixture()
def output_dir(tmp_path: Path) -> Path:
    """Provide a clean output directory."""
    out = tmp_path / "project"
    return out


# ---------------------------------------------------------------------------
# Custom filter tests
# ---------------------------------------------------------------------------


class TestCustomFilters:
    """Tests for Jinja2 custom filters."""

    def test_slugify_basic(self) -> None:
        assert _slugify("Acme Bank FSI") == "acme-bank-fsi"

    def test_slugify_special_chars(self) -> None:
        assert _slugify("Hello, World!") == "hello-world"

    def test_slugify_already_slug(self) -> None:
        assert _slugify("already-a-slug") == "already-a-slug"

    def test_slugify_underscores(self) -> None:
        assert _slugify("with_underscores_here") == "with-underscores-here"

    def test_snake_case_basic(self) -> None:
        assert _snake_case("Acme Bank FSI") == "acme_bank_fsi"

    def test_snake_case_hyphens(self) -> None:
        assert _snake_case("acme-bank-fsi") == "acme_bank_fsi"

    def test_snake_case_already_snake(self) -> None:
        assert _snake_case("already_snake") == "already_snake"

    def test_title_case_basic(self) -> None:
        assert _title_case("acme-bank-fsi") == "Acme Bank Fsi"

    def test_title_case_underscores(self) -> None:
        assert _title_case("data_foundation") == "Data Foundation"

    def test_title_case_spaces(self) -> None:
        assert _title_case("hello world") == "Hello World"


# ---------------------------------------------------------------------------
# Template path mapping tests
# ---------------------------------------------------------------------------


class TestTemplatePathMapping:
    """Tests for _map_template_path."""

    def test_root_prefix_stripped(self) -> None:
        assert _map_template_path("root/CLAUDE.md.j2") == "CLAUDE.md"

    def test_claude_maps_to_dot_claude(self) -> None:
        assert (
            _map_template_path("claude/agents/lakehouse-dev.md.j2")
            == ".claude/agents/lakehouse-dev.md"
        )

    def test_conf_passes_through(self) -> None:
        assert _map_template_path("conf/databricks.yml.j2") == "conf/databricks.yml"

    def test_docs_passes_through(self) -> None:
        assert _map_template_path("docs/PROGRESS.md.j2") == "docs/PROGRESS.md"

    def test_scripts_passes_through(self) -> None:
        assert _map_template_path("scripts/deploy.sh.j2") == "scripts/deploy.sh"

    def test_j2_extension_stripped(self) -> None:
        assert _map_template_path("root/file.txt.j2") == "file.txt"

    def test_double_j2_strips_only_suffix(self) -> None:
        assert _map_template_path("root/data.j2.j2") == "data.j2"


# ---------------------------------------------------------------------------
# Jinja2 environment tests
# ---------------------------------------------------------------------------


class TestJinjaEnvironment:
    """Tests for _build_jinja_env configuration."""

    def test_strict_undefined(self, templates_dir: Path) -> None:
        """Missing variables raise UndefinedError."""
        env = _build_jinja_env(templates_dir)
        template = env.from_string("{{ nonexistent_var }}")
        with pytest.raises(UndefinedError):
            template.render()

    def test_custom_filters_registered(self, templates_dir: Path) -> None:
        """All three custom filters are available."""
        env = _build_jinja_env(templates_dir)
        assert "slugify" in env.filters
        assert "snake_case" in env.filters
        assert "title_case" in env.filters

    def test_slugify_filter_in_template(self, templates_dir: Path) -> None:
        """slugify filter works inside a rendered template."""
        env = _build_jinja_env(templates_dir)
        template = env.from_string("{{ value | slugify }}")
        assert template.render(value="Hello World") == "hello-world"

    def test_snake_case_filter_in_template(self, templates_dir: Path) -> None:
        """snake_case filter works inside a rendered template."""
        env = _build_jinja_env(templates_dir)
        template = env.from_string("{{ value | snake_case }}")
        assert template.render(value="Hello World") == "hello_world"

    def test_title_case_filter_in_template(self, templates_dir: Path) -> None:
        """title_case filter works inside a rendered template."""
        env = _build_jinja_env(templates_dir)
        template = env.from_string("{{ value | title_case }}")
        assert template.render(value="hello-world") == "Hello World"

    def test_trim_blocks(self, templates_dir: Path) -> None:
        """trim_blocks is enabled (removes newline after block tags)."""
        env = _build_jinja_env(templates_dir)
        assert env.trim_blocks is True

    def test_lstrip_blocks(self, templates_dir: Path) -> None:
        """lstrip_blocks is enabled."""
        env = _build_jinja_env(templates_dir)
        assert env.lstrip_blocks is True


# ---------------------------------------------------------------------------
# Directory structure tests
# ---------------------------------------------------------------------------


class TestDirectoryCreation:
    """Tests for _create_directories."""

    def test_creates_conf(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / "conf").is_dir()

    def test_creates_lakehouse_dirs(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / "src" / "data_generation" / "structured").is_dir()
        assert (output_dir / "src" / "data_generation" / "unstructured").is_dir()
        assert (output_dir / "src" / "pipelines" / "bronze").is_dir()
        assert (output_dir / "src" / "pipelines" / "silver").is_dir()
        assert (output_dir / "src" / "pipelines" / "gold").is_dir()
        assert (output_dir / "src" / "ml").is_dir()
        assert (output_dir / "src" / "dashboards").is_dir()
        assert (output_dir / "src" / "agents").is_dir()

    def test_creates_docs_dir(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / "docs").is_dir()
        assert (output_dir / "docs" / "supplements").is_dir()

    def test_creates_scripts_dir(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / "scripts").is_dir()

    def test_creates_claude_dirs(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / ".claude" / "agents").is_dir()
        assert (output_dir / ".claude" / "commands").is_dir()
        assert (output_dir / ".claude" / "skills").is_dir()

    def test_creates_resources_dirs(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / "resources" / "jobs").is_dir()
        assert (output_dir / "resources" / "pipelines").is_dir()
        assert (output_dir / "resources" / "dashboards").is_dir()

    def test_creates_agent_memory_dirs(self, output_dir: Path) -> None:
        output_dir.mkdir(parents=True)
        _create_directories(output_dir)
        assert (output_dir / ".claude" / "agent-memory" / "lakehouse-dev").is_dir()
        assert (output_dir / ".claude" / "agent-memory" / "app-dev").is_dir()
        assert (output_dir / ".claude" / "agent-memory" / "dashboard-dev").is_dir()
        assert (output_dir / ".claude" / "agent-memory" / "reviewer").is_dir()


# ---------------------------------------------------------------------------
# generate() integration tests
# ---------------------------------------------------------------------------


class TestGenerate:
    """Tests for the generate() orchestrator."""

    def test_returns_scaffold_result(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """generate() returns a ScaffoldResult."""
        result = generate(sample_context, output_dir, templates_dir=templates_dir)
        assert isinstance(result, ScaffoldResult)

    def test_project_dir_matches(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """Result project_dir matches the output_dir."""
        result = generate(sample_context, output_dir, templates_dir=templates_dir)
        assert result.project_dir == output_dir

    def test_creates_output_directory(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """generate() creates the output directory if it doesn't exist."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        assert output_dir.is_dir()

    def test_renders_claude_md(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """CLAUDE.md is rendered from root/CLAUDE.md.j2 with variable substitution."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        claude_md = output_dir / "CLAUDE.md"
        assert claude_md.exists()
        content = claude_md.read_text()
        assert "Acme Bank" in content
        assert "acme-bank-fsi-demo" in content

    def test_renders_env_example(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """.env.example is rendered with context variables."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        env_file = output_dir / ".env.example"
        assert env_file.exists()
        content = env_file.read_text()
        assert "COMPANY=Acme Bank" in content
        assert "INDUSTRY=fsi" in content

    def test_renders_databricks_yml(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """databricks.yml is rendered at project root with correct values."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        yml_file = output_dir / "databricks.yml"
        assert yml_file.exists()
        content = yml_file.read_text()
        assert "acme-bank-fsi-demo" in content
        assert "adb-1234567890" in content

    def test_renders_claude_agent(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """.claude/agents/lakehouse-dev.md is rendered in the correct location."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        agent_file = output_dir / ".claude" / "agents" / "lakehouse-dev.md"
        assert agent_file.exists()
        content = agent_file.read_text()
        assert "Acme Bank" in content

    def test_renders_custom_filters(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """Templates using custom filters render correctly."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        slug_file = output_dir / "slugified.txt"
        assert slug_file.exists()
        content = slug_file.read_text()
        assert "acme-bank" in content  # slugify
        assert "acme_bank" in content  # snake_case
        assert "Acme Bank" in content  # title_case

    def test_files_created_list_populated(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """files_created contains all rendered templates plus manifest."""
        result = generate(sample_context, output_dir, templates_dir=templates_dir)
        assert len(result.files_created) > 0
        assert "conf/demo_manifest.json" in result.files_created

    def test_manifest_written_to_conf(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """demo_manifest.json is written to conf/ and is valid JSON."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        manifest_path = output_dir / "conf" / "demo_manifest.json"
        assert manifest_path.exists()
        data = json.loads(manifest_path.read_text())
        assert data["meta"]["company"] == "Acme Bank"
        assert len(data["waves"]) == 5

    def test_manifest_matches_model(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """Written manifest can be deserialised back to a DemoManifest."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        manifest_path = output_dir / "conf" / "demo_manifest.json"
        data = json.loads(manifest_path.read_text())
        manifest = DemoManifest.model_validate(data)
        assert manifest.meta.company == "Acme Bank"
        assert all(w.status == "pending" for w in manifest.waves)

    def test_result_manifest_has_five_waves(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """The returned ScaffoldResult manifest has 5 waves."""
        result = generate(sample_context, output_dir, templates_dir=templates_dir)
        assert len(result.manifest.waves) == 5

    def test_lakehouse_stub_dirs_exist(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """All lakehouse stub directories are created."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        assert (output_dir / "src" / "data_generation" / "structured").is_dir()
        assert (output_dir / "src" / "pipelines" / "bronze").is_dir()
        assert (output_dir / "src" / "pipelines" / "silver").is_dir()
        assert (output_dir / "src" / "pipelines" / "gold").is_dir()
        assert (output_dir / "src" / "ml").is_dir()
        assert (output_dir / "src" / "dashboards").is_dir()
        assert (output_dir / "src" / "agents").is_dir()

    def test_no_warnings_when_templates_exist(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """No warnings when templates are found."""
        result = generate(sample_context, output_dir, templates_dir=templates_dir)
        assert len(result.warnings) == 0

    def test_warning_when_no_templates(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        tmp_path: Path,
    ) -> None:
        """Warning generated when no .j2 templates are found."""
        empty_tpl = tmp_path / "empty_templates"
        empty_tpl.mkdir()
        result = generate(sample_context, output_dir, templates_dir=empty_tpl)
        assert len(result.warnings) == 1
        assert "No .j2 templates found" in result.warnings[0]


class TestIndustryDisplayNames:
    """Tests for INDUSTRY_DISPLAY_NAMES mapping."""

    def test_has_fsi(self) -> None:
        assert "fsi" in INDUSTRY_DISPLAY_NAMES

    def test_has_telecom(self) -> None:
        assert "telecom" in INDUSTRY_DISPLAY_NAMES

    def test_has_retail(self) -> None:
        assert "retail" in INDUSTRY_DISPLAY_NAMES

    def test_has_healthcare(self) -> None:
        assert "healthcare" in INDUSTRY_DISPLAY_NAMES

    def test_has_manufacturing(self) -> None:
        assert "manufacturing" in INDUSTRY_DISPLAY_NAMES

    def test_fsi_display_name(self) -> None:
        assert INDUSTRY_DISPLAY_NAMES["fsi"] == "Financial Services"


class TestDisplayNameFallback:
    """Tests for display_name when company is empty."""

    def test_empty_company_uses_industry_display_name(
        self,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """When company is empty, display_name uses INDUSTRY_DISPLAY_NAMES."""
        context = ScaffoldContext(
            company="",
            industry="fsi",
            demo_description="Test",
            catalog="fsi_demo",
            warehouse_id="wh123",
            workspace_host="https://test.databricks.net",
            notification_email="test@test.com",
            bundle_name="fsi-demo",
            app_name="fsi-app",
        )
        result = generate(context, output_dir, templates_dir=templates_dir)
        # The CLAUDE.md template uses display_name; verify it got "Financial Services"
        claude_md = output_dir / "CLAUDE.md"
        assert claude_md.exists()
        # We can't check substitution in the test template directly,
        # but we verify the generate function didn't error out
        assert isinstance(result, ScaffoldResult)


class TestStubDirectories:
    """Tests for _STUB_DIRECTORIES completeness."""

    def test_includes_agent_memory_dirs(self) -> None:
        """_STUB_DIRECTORIES must include agent-memory subdirs."""
        assert ".claude/agent-memory/lakehouse-dev" in _STUB_DIRECTORIES
        assert ".claude/agent-memory/app-dev" in _STUB_DIRECTORIES
        assert ".claude/agent-memory/dashboard-dev" in _STUB_DIRECTORIES
        assert ".claude/agent-memory/reviewer" in _STUB_DIRECTORIES

    def test_includes_resources_dashboards(self) -> None:
        """_STUB_DIRECTORIES must include resources/dashboards."""
        assert "resources/dashboards" in _STUB_DIRECTORIES


class TestGitkeepFiles:
    """Tests for .gitkeep files in agent-memory directories."""

    def test_gitkeep_files_created_in_agent_memory(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """generate() must create .gitkeep files in all agent-memory dirs."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        for agent_dir in ["lakehouse-dev", "app-dev", "dashboard-dev", "reviewer"]:
            gitkeep = output_dir / ".claude" / "agent-memory" / agent_dir / ".gitkeep"
            assert gitkeep.exists(), f".gitkeep missing in agent-memory/{agent_dir}"
            assert gitkeep.read_text() == ""

    def test_gitkeep_files_are_empty(
        self,
        sample_context: ScaffoldContext,
        output_dir: Path,
        templates_dir: Path,
    ) -> None:
        """All .gitkeep files must be empty."""
        generate(sample_context, output_dir, templates_dir=templates_dir)
        for agent_dir in ["lakehouse-dev", "app-dev", "dashboard-dev", "reviewer"]:
            gitkeep = output_dir / ".claude" / "agent-memory" / agent_dir / ".gitkeep"
            assert gitkeep.stat().st_size == 0
