"""Tests for the Shape IR reference converter."""
from __future__ import annotations

import json
import zipfile
from pathlib import Path

import pytest

from aieng.converters.base import ConverterError
from aieng.converters.cli_runners import convert_source
from aieng.converters.registry import available_converters
from aieng.converters.shape_ir import ShapeIRConverter
from aieng.validate import validate_package


def _write_shape_ir(path: Path) -> Path:
    payload = {
        "format_version": "0.1.0",
        "model_id": "organic_shell_demo",
        "model_kind": "organic",
        "parts": [
            {
                "id": "helmet_shell",
                "name": "Helmet shell",
                "kind": "freeform",
                "feature_type": "unknown_feature",
                "surface_type": "bspline",
                "freeform": True,
                "bbox": [-50, -35, 0, 50, 35, 80],
                "proxy_normal": [0, 0, 1],
                "uv_bounds": [0, 1, 0, 1],
                "curvature_sample": {"status": "not_computed"},
                "parameters": {
                    "section_count": 5,
                    "shell_offset_mm": 2.5,
                },
            },
            {
                "id": "visor_interface",
                "name": "Visor interface",
                "kind": "trim_patch",
                "feature_type": "interface_face",
                "surface_type": "plane",
                "bbox": [-30, -36, 35, 30, -34, 55],
                "normal": [0, -1, 0],
            },
        ],
        "adjacency": [
            {"source": "helmet_shell", "target": "visor_interface", "type": "trimmed_by"},
        ],
    }
    path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
    return path


def test_shape_ir_converter_is_registered():
    assert "shape_ir_reference" in available_converters()


def test_shape_ir_converter_projects_topology_and_features(tmp_path: Path):
    source = _write_shape_ir(tmp_path / "organic_shell.shape.json")
    converter = ShapeIRConverter()

    result = converter.convert(source, model_id="organic_shell")

    assert result.converter_id == "shape_ir_reference"
    assert result.source_system == "AIENG Shape IR"
    assert result.runtime_mode == "offline"
    assert result.source_content_sha256 is not None
    assert {level.level for level in result.achieved_levels} == {0, 1, 2, 3, 4}
    assert "geometry/source.py" in result.package_files
    source_py = result.package_files["geometry/source.py"].decode()
    compile(source_py, "geometry/source.py", "exec")
    assert "part_helmet_shell" in source_py
    assert "result = Compound(children=parts)" in source_py

    topology = json.loads(result.package_files["geometry/topology_map.json"])
    assert topology["metadata"]["extraction_mode"] == "projected_from_shape_ir"
    assert topology["metadata"]["real_step_parsing"] is False
    assert any(entity["id"] == "face_helmet_shell" and entity.get("freeform") is True for entity in topology["entities"])
    face = next(entity for entity in topology["entities"] if entity["id"] == "face_helmet_shell")
    assert face["adjacent_entity_ids"] == ["face_visor_interface"]
    assert face["surface_type"] == "bspline"
    assert face["proxy_normal"] == [0, 0, 1]
    assert face["uv_bounds"] == [0, 1, 0, 1]
    assert face["curvature_sample"] == {"status": "not_computed"}

    feature_graph = json.loads(result.package_files["graph/feature_graph.json"])
    features = {feature["id"]: feature for feature in feature_graph["features"]}
    assert features["feat_helmet_shell"]["editability"] == "proposal_allowed"
    assert features["feat_helmet_shell"]["writeback_strategy"] == "semantic_parameter_update_only"
    assert features["feat_visor_interface"]["type"] == "interface_face"


def test_convert_source_produces_valid_shape_ir_package(tmp_path: Path):
    source = _write_shape_ir(tmp_path / "organic_shell.shape_ir.json")
    out_path = tmp_path / "organic_shell.aieng"

    convert_source(
        source_path=source,
        out=out_path,
        model_id="organic_shell",
        converter_id=None,
        overwrite=True,
        runtime_mode="offline",
    )

    with zipfile.ZipFile(out_path) as archive:
        names = set(archive.namelist())
        assert "geometry/shape_ir.json" in names
        assert "geometry/source.py" in names
        assert "geometry/topology_map.json" in names
        assert "graph/feature_graph.json" in names
        assert "objects/object_registry.json" in names
        assert "provenance/conversion_manifest.json" in names
        assert "provenance/converter_capabilities.json" in names
        manifest = json.loads(archive.read("manifest.json"))
        assert manifest["source_mode"] == "converter"
        assert manifest["created_by"]["converter_id"] == "shape_ir_reference"
        conversion_manifest = json.loads(archive.read("provenance/conversion_manifest.json"))
        emitted_paths = {item["path"] for item in conversion_manifest["emitted_resources"]}
        assert "geometry/source.py" in emitted_paths
        coverage = {item["category"]: item["status"] for item in conversion_manifest["coverage_categories"]}
        assert coverage["geometry"] == "inferred"
        assert coverage["topology"] == "inferred"
        assert coverage["writeback_metadata"] == "unsupported"

    report = validate_package(out_path)
    fails = [message for message in report.messages if message.level.value == "FAIL"]
    assert not fails, f"Shape IR package should validate; fails: {[m.text for m in fails]}"


def test_shape_ir_converter_rejects_invalid_source(tmp_path: Path):
    source = tmp_path / "broken.shape.json"
    source.write_text("[]", encoding="utf-8")
    with pytest.raises(ConverterError):
        ShapeIRConverter().convert(source, model_id="broken")


def test_shape_ir_compiler_supports_type_key_and_organic_blend():
    from aieng.converters.shape_ir import compile_shape_ir_to_build123d_source

    payload = {
        "parts": [
            {
                "id": "torso",
                "type": "lofted_stack",
                "sections": [[0, 120, 80], [200, 150, 90], [392, 60]],
            },
            {
                "id": "head",
                "type": "sphere",
                "radius": 45,
                "location": [0, 0, 440],
            },
            {
                "id": "body",
                "type": "organic_blend",
                "children": ["torso", "head"],
                "radius": 12,
            },
        ],
    }

    source = compile_shape_ir_to_build123d_source(payload)
    compile(source, "geometry/source.py", "exec")
    assert "lofted_stack([[0, 120, 80], [200, 150, 90], [392, 60]], label='torso')" in source
    assert "organic_blend([part_torso, part_head], 12.0, label='body')" in source
    # Referenced children are compiled as intermediate solids but not duplicated
    # into the final Compound unless emit=true is explicitly requested.
    assert "parts.append(part_torso)" not in source
    assert "parts.append(part_head)" not in source
    assert "parts.append(part_body)" in source


def test_shape_ir_compiler_combines_translation_and_rotation():
    """A node with both location and rotation must compile to a SINGLE
    Location(translation, rotation) — not translate-then-rotate-about-origin,
    which would orbit the placed part around the world origin."""
    from aieng.converters.shape_ir import compile_shape_ir_to_build123d_source

    payload = {
        "parts": [
            {
                "id": "arm",
                "type": "box",
                "dimensions": [10, 10, 40],
                "location": [100, 0, 200],
                "rotation": [0, 90, 0],
            },
        ],
    }
    source = compile_shape_ir_to_build123d_source(payload)
    compile(source, "geometry/source.py", "exec")
    # single combined Location with both translation and rotation
    assert "part_arm = part_arm.moved(Location((100.0, 0.0, 200.0), (0.0, 90.0, 0.0)))" in source
    # the old buggy pattern (separate rotate about origin) must be gone
    assert "Location((0, 0, 0)," not in source
    # exactly one .moved() for this part
    assert source.count("part_arm = part_arm.moved(") == 1


def test_shape_ir_sdf_compiler_emits_valid_source():
    """implicit_sdf compiler produces syntactically valid fogleman/sdf source:
    primitives, smooth blend (union k=), boolean subtract, and translate."""
    from aieng.converters.shape_ir_sdf import compile_shape_ir_to_sdf_source

    payload = {
        "representation": "implicit_sdf",
        "parts": [
            {"id": "a", "type": "sphere", "radius": 20},
            {"id": "b", "type": "sphere", "radius": 15, "location": [0, 0, 30]},
            {"id": "blob", "type": "organic_blend", "children": ["a", "b"], "radius": 5},
            {"id": "slot", "type": "box", "dimensions": [6, 6, 60], "operation": "subtract"},
        ],
    }
    src = compile_shape_ir_to_sdf_source(payload)
    compile(src, "geometry/sdf_source.py", "exec")  # syntax-valid
    assert src.startswith("from sdf import *")
    assert "f_a = sphere(20.0)" in src
    assert "f_b = sphere(15.0)" in src
    assert "f_b = f_b.translate((0.0, 0.0, 30.0))" in src
    assert "f_blob = f_a.union(f_b, k=5.0)" in src
    assert "f_slot = box((6.0, 6.0, 60.0))" in src
    assert "f = (f_blob - f_slot)" in src
    assert "parts.append" not in src  # that's the build123d target, not SDF


def test_compile_shape_ir_dispatch_routes_by_representation():
    from aieng.converters.shape_ir import compile_shape_ir

    sdf = compile_shape_ir({"representation": "implicit_sdf", "parts": [{"id": "s", "type": "sphere", "radius": 5}]})
    assert sdf["representation"] == "implicit_sdf"
    assert sdf["source_path"] == "geometry/sdf_source.py"
    assert sdf["runtime"] == "sdf" and sdf["fallback"] is False
    assert "from sdf import" in sdf["source"]

    brep = compile_shape_ir({"parts": [{"id": "b", "type": "box", "dimensions": [1, 1, 1]}]})
    assert brep["representation"] == "brep_build123d"
    assert brep["source_path"] == "geometry/source.py"
    assert "from build123d import" in brep["source"] and brep["fallback"] is False

    # unknown representation falls back to build123d and flags it
    unknown = compile_shape_ir({"representation": "nurbs_compas", "parts": [{"id": "b", "type": "box", "dimensions": [1, 1, 1]}]})
    assert unknown["representation"] == "brep_build123d"
    assert unknown["requested_representation"] == "nurbs_compas"
    assert unknown["fallback"] is True


def test_shape_ir_converter_implicit_sdf_emits_sdf_source(tmp_path: Path):
    """An implicit_sdf Shape IR converts to geometry/sdf_source.py (not source.py),
    with honest representation metadata."""
    payload = {
        "representation": "implicit_sdf",
        "model_id": "blob",
        "parts": [
            {"id": "a", "type": "sphere", "radius": 10},
            {"id": "b", "type": "sphere", "radius": 8, "location": [0, 0, 15]},
            {"id": "blob", "type": "organic_blend", "children": ["a", "b"], "radius": 3},
        ],
    }
    p = tmp_path / "blob.shape_ir.json"
    p.write_text(json.dumps(payload), encoding="utf-8")
    result = ShapeIRConverter().convert(p, model_id="blob")

    assert "geometry/sdf_source.py" in result.package_files
    assert "geometry/source.py" not in result.package_files
    assert b"from sdf import *" in result.package_files["geometry/sdf_source.py"]
    md = result.source_document_metadata
    assert md["representation"] == "implicit_sdf"
    assert md["compile_runtime"] == "sdf"
    assert md["build123d_source_emitted"] is False
    assert md["representation_fallback"] is False


def test_shape_ir_manifold_compiler_emits_valid_source():
    """manifold_mesh compiler produces syntactically valid manifold3d source:
    primitives, boolean subtract, native rotation+translate."""
    from aieng.converters.shape_ir_manifold import compile_shape_ir_to_manifold_source

    payload = {
        "representation": "manifold_mesh",
        "parts": [
            {"id": "body", "type": "sphere", "radius": 20},
            {"id": "slot", "type": "box", "dimensions": [6, 6, 60],
             "operation": "subtract", "location": [0, 0, 5], "rotation": [0, 90, 0]},
        ],
    }
    src = compile_shape_ir_to_manifold_source(payload)
    compile(src, "geometry/manifold_source.py", "exec")
    assert src.startswith("from manifold3d import Manifold")
    assert "m_body = Manifold.sphere(20.0)" in src
    assert "Manifold.cube((6.0, 6.0, 60.0), True)" in src
    assert "m_slot = m_slot.rotate((0.0, 90.0, 0.0))" in src
    assert "m_slot = m_slot.translate((0.0, 0.0, 5.0))" in src
    assert "result = (m_body - m_slot)" in src


def test_compile_shape_ir_dispatch_manifold_and_registry():
    from aieng.converters.shape_ir import compile_shape_ir, available_representations

    man = compile_shape_ir({"representation": "manifold_mesh", "parts": [{"id": "s", "type": "sphere", "radius": 5}]})
    assert man["representation"] == "manifold_mesh"
    assert man["source_path"] == "geometry/manifold_source.py"
    assert man["runtime"] == "manifold" and man["fallback"] is False
    assert "from manifold3d import Manifold" in man["source"]
    # registry exposes all three built-in targets
    reps = available_representations()
    assert {"brep_build123d", "implicit_sdf", "manifold_mesh"} <= set(reps)


def test_shape_ir_nurbs_compiler_emits_valid_source():
    """nurbs_brep compiler emits build123d source that builds a NURBS face via OCP."""
    from aieng.converters.shape_ir_nurbs import compile_shape_ir_to_nurbs_source

    payload = {
        "representation": "nurbs_brep",
        "parts": [
            {"id": "patch", "type": "nurbs_surface", "control_net": [
                [[0, 0, 0], [10, 0, 2], [20, 0, 0]],
                [[0, 10, 2], [10, 10, 5], [20, 10, 2]],
                [[0, 20, 0], [10, 20, 2], [20, 20, 0]],
            ]},
        ],
    }
    src = compile_shape_ir_to_nurbs_source(payload)
    compile(src, "geometry/source.py", "exec")
    assert src.startswith("from build123d import *")
    assert "def _nurbs_face(" in src
    assert "GeomAPI_PointsToBSplineSurface" in src
    assert "part_patch = _nurbs_face([[[0, 0, 0]" in src
    assert "result = parts[0]" in src or "result = Compound" in src


def test_compile_shape_ir_dispatch_nurbs_runs_on_build123d():
    from aieng.converters.shape_ir import compile_shape_ir, representation_runtime, available_representations

    out = compile_shape_ir({"representation": "nurbs_brep", "parts": [
        {"id": "p", "type": "nurbs_surface", "control_net": [[[0, 0, 0], [1, 0, 0]], [[0, 1, 0], [1, 1, 1]]]},
    ]})
    assert out["representation"] == "nurbs_brep"
    assert out["source_path"] == "geometry/source.py"   # reuses the build123d runner
    assert out["runtime"] == "build123d" and out["fallback"] is False
    assert "_nurbs_face(" in out["source"]
    # runtime routing
    assert representation_runtime("nurbs_brep") == "build123d"
    assert representation_runtime("implicit_sdf") == "sdf"
    assert representation_runtime("manifold_mesh") == "manifold"
    assert "nurbs_brep" in available_representations()
