"""Tests for the JSON renderer module."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

from hld_generator.graph import GraphBuilder, CodeGraph
from hld_generator.json_renderer import JSONRenderer
from hld_generator.llm import LLMAnalysis
from hld_generator.parsers.base import ParsedFile, ParsedEntity, ImportInfo, EntityType


def _make_parsed(path: str, lang: str, imports: list[str] | None = None,
                 classes: list[str] | None = None, functions: list[str] | None = None) -> ParsedFile:
    pf = ParsedFile(file_path=path, language=lang, line_count=10)
    for cls in (classes or []):
        pf.entities.append(ParsedEntity(name=cls, entity_type=EntityType.CLASS, line_start=1, line_end=5))
    for fn in (functions or []):
        pf.entities.append(ParsedEntity(name=fn, entity_type=EntityType.FUNCTION, line_start=1, line_end=5))
    for imp in (imports or []):
        pf.imports.append(ImportInfo(module=imp, names=[]))
    return pf


def _make_analysis(**overrides) -> LLMAnalysis:
    defaults = {
        "project_name": "TestProject",
        "summary": "A test project",
        "architecture_pattern": "layered",
        "components": [
            {"name": "core", "layer": "Business Logic", "purpose": "Core logic", "key_modules": ["core.py"]},
        ],
        "data_flow": [{"from": "api", "to": "core", "description": "requests"}],
        "external_dependencies": [{"name": "requests", "purpose": "HTTP"}],
        "tech_stack": {"language": "Python", "framework": "Flask"},
        "mermaid_diagram": "graph TD\n    A --> B",
        "observations": ["Well-structured"],
    }
    defaults.update(overrides)
    return LLMAnalysis(**defaults)


def _make_code_graph(files: list[ParsedFile] | None = None) -> CodeGraph:
    if files is None:
        files = [
            _make_parsed("src/api.py", "python", imports=["core"], functions=["handle_request"]),
            _make_parsed("src/core.py", "python", classes=["Engine"]),
        ]
    return GraphBuilder().build(files)


class TestJSONRendererOutput:
    """Test that JSONRenderer produces valid JSON with all required fields."""

    def test_renders_valid_json_file(self):
        with tempfile.TemporaryDirectory() as d:
            out = Path(d)
            renderer = JSONRenderer(out)
            analysis = _make_analysis()
            cg = _make_code_graph()
            paths = renderer.render(analysis, cg)

            assert len(paths) >= 1
            json_path = paths[0]
            assert json_path.name == "hld_data.json"
            assert json_path.exists()

            # Viewer files should also be generated
            viewer_path = out / "viewer.html"
            assert viewer_path.exists(), "viewer.html should be generated"
            assert (out / "assets").is_dir(), "assets/ directory should be copied"

            data = json.loads(json_path.read_text(encoding="utf-8"))
            assert isinstance(data, dict)

    def test_contains_all_top_level_fields(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            data = renderer._serialize(_make_analysis(), _make_code_graph())

            required_fields = [
                "version", "generated_at", "generator_version",
                "project_name", "summary", "architecture_pattern",
                "components", "data_flow", "external_dependencies",
                "tech_stack", "mermaid_diagram", "observations",
                "error", "graph",
            ]
            for field in required_fields:
                assert field in data, f"Missing top-level field: {field}"

    def test_version_is_string(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            data = renderer._serialize(_make_analysis(), _make_code_graph())
            assert isinstance(data["version"], str)
            assert data["version"] == "1.0"

    def test_project_name_matches_analysis(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            analysis = _make_analysis(project_name="MyApp")
            data = renderer._serialize(analysis, _make_code_graph())
            assert data["project_name"] == "MyApp"

    def test_components_are_list(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            data = renderer._serialize(_make_analysis(), _make_code_graph())
            assert isinstance(data["components"], list)
            assert len(data["components"]) > 0

    def test_graph_contains_modules_and_edges(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            data = renderer._serialize(_make_analysis(), _make_code_graph())
            graph = data["graph"]
            assert "modules" in graph
            assert "edges" in graph
            assert "packages" in graph
            assert "languages" in graph
            assert isinstance(graph["modules"], dict)
            assert isinstance(graph["edges"], list)

    def test_edges_have_source_target_label(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            files = [
                _make_parsed("a.py", "python", imports=["b"]),
                _make_parsed("b.py", "python"),
            ]
            cg = GraphBuilder().build(files)
            data = renderer._serialize(_make_analysis(), cg)
            edges = data["graph"]["edges"]
            if edges:
                edge = edges[0]
                assert "source" in edge
                assert "target" in edge
                assert "label" in edge


class TestJSONRendererEdgeCases:
    """Test edge cases: empty data, Unicode, missing fields."""

    def test_empty_components(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            analysis = _make_analysis(components=[])
            data = renderer._serialize(analysis, _make_code_graph())
            assert data["components"] == []

    def test_empty_graph_no_files(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            cg = CodeGraph()
            data = renderer._serialize(_make_analysis(), cg)
            assert data["graph"]["modules"] == {}
            assert data["graph"]["edges"] == []

    def test_unicode_component_names(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            analysis = _make_analysis(
                project_name="Projet Francais",
                components=[{"name": "modele", "layer": "Data", "purpose": "Donnees", "key_modules": ["m.py"]}],
            )
            paths = renderer.render(analysis, _make_code_graph())
            raw = paths[0].read_text(encoding="utf-8")
            data = json.loads(raw)
            assert data["project_name"] == "Projet Francais"
            assert data["components"][0]["name"] == "modele"

    def test_error_field_is_none_when_no_error(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            analysis = _make_analysis()
            analysis.error = ""
            data = renderer._serialize(analysis, _make_code_graph())
            assert data["error"] is None

    def test_error_field_is_string_when_error_present(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            analysis = _make_analysis()
            analysis.error = "LLM call failed"
            data = renderer._serialize(analysis, _make_code_graph())
            assert data["error"] == "LLM call failed"

    def test_mermaid_diagram_empty_string_when_none(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            analysis = _make_analysis(mermaid_diagram="")
            analysis.mermaid_diagram = None  # type: ignore
            data = renderer._serialize(analysis, _make_code_graph())
            # Should be empty string, not None
            assert data["mermaid_diagram"] == "" or data["mermaid_diagram"] is None

    def test_creates_output_directory_if_missing(self):
        with tempfile.TemporaryDirectory() as d:
            out = Path(d) / "nested" / "output"
            JSONRenderer(out)
            assert out.is_dir()

    def test_generated_at_is_iso_format(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            data = renderer._serialize(_make_analysis(), _make_code_graph())
            from datetime import datetime
            # Should not raise
            datetime.fromisoformat(data["generated_at"])

    def test_module_fields_are_complete(self):
        with tempfile.TemporaryDirectory() as d:
            renderer = JSONRenderer(Path(d))
            cg = _make_code_graph()
            data = renderer._serialize(_make_analysis(), cg)
            for mod_id, mod in data["graph"]["modules"].items():
                assert "id" in mod
                assert "language" in mod
                assert "package" in mod
                assert "classes" in mod
                assert "functions" in mod
                assert "line_count" in mod
