"""black-box tests for model generation."""

import json
import tempfile
from pathlib import Path


class TestGenerate:
    """test the generate function end-to-end."""

    def test_generate_simple_record(self):
        """generate a pydantic model from a simple record lexicon."""
        from pmgfal import generate

        lexicon = {
            "lexicon": 1,
            "id": "fm.plyr.track",
            "description": "a music track",
            "defs": {
                "main": {
                    "type": "record",
                    "description": "track record",
                    "record": {
                        "type": "object",
                        "properties": {
                            "uri": {"type": "string"},
                            "title": {"type": "string"},
                            "artist": {"type": "string"},
                            "durationMs": {"type": "integer"},
                        },
                        "required": ["uri", "title", "artist"],
                    },
                }
            },
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            (lexicon_dir / "track.json").write_text(json.dumps(lexicon))

            output_dir = Path(tmpdir) / "generated"
            files = generate(str(lexicon_dir), str(output_dir))

            assert len(files) == 1
            content = Path(files[0]).read_text()

            # verify header
            assert "auto-generated by pmgfal" in content

            # verify class exists
            assert "class FmPlyrTrack(BaseModel):" in content

            # verify required fields
            assert "uri: str" in content
            assert "title: str" in content
            assert "artist: str" in content

            # verify optional field with alias
            assert "duration_ms" in content
            assert "durationMs" in content

    def test_generate_with_namespace_filter(self):
        """filter by namespace prefix."""
        from pmgfal import generate

        lexicons = [
            {
                "lexicon": 1,
                "id": "fm.plyr.track",
                "defs": {
                    "main": {
                        "type": "record",
                        "record": {"type": "object", "properties": {}},
                    }
                },
            },
            {
                "lexicon": 1,
                "id": "com.other.thing",
                "defs": {
                    "main": {
                        "type": "record",
                        "record": {"type": "object", "properties": {}},
                    }
                },
            },
        ]

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()

            for lex in lexicons:
                name = lex["id"].replace(".", "_")
                (lexicon_dir / f"{name}.json").write_text(json.dumps(lex))

            output_dir = Path(tmpdir) / "generated"
            files = generate(str(lexicon_dir), str(output_dir), "fm.plyr")

            assert len(files) == 1
            content = Path(files[0]).read_text()
            assert "FmPlyrTrack" in content
            assert "ComOtherThing" not in content

    def test_generate_empty_dir(self):
        """empty directory returns empty list."""
        from pmgfal import generate

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            output_dir = Path(tmpdir) / "generated"

            files = generate(str(lexicon_dir), str(output_dir))
            assert files == []

    def test_generated_models_are_valid_pydantic(self):
        """generated models should be importable and work with pydantic."""
        from pmgfal import generate

        lexicon = {
            "lexicon": 1,
            "id": "test.example",
            "defs": {
                "main": {
                    "type": "record",
                    "record": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "count": {"type": "integer"},
                        },
                        "required": ["name"],
                    },
                }
            },
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            (lexicon_dir / "example.json").write_text(json.dumps(lexicon))

            output_dir = Path(tmpdir) / "generated"
            files = generate(str(lexicon_dir), str(output_dir))

            # exec the generated code
            content = Path(files[0]).read_text()
            namespace = {}
            exec(content, namespace)

            # verify class exists and works
            TestExample = namespace["TestExample"]
            instance = TestExample(name="test", count=42)
            assert instance.name == "test"
            assert instance.count == 42

            # verify optional field default
            instance2 = TestExample(name="test")
            assert instance2.count is None

    def test_internal_ref_resolution(self):
        """internal refs like #subDef should resolve to class names."""
        from pmgfal import generate

        lexicon = {
            "lexicon": 1,
            "id": "fm.plyr.track",
            "defs": {
                "main": {
                    "type": "record",
                    "record": {
                        "type": "object",
                        "properties": {
                            "title": {"type": "string"},
                            "features": {
                                "type": "array",
                                "items": {"type": "ref", "ref": "#featuredArtist"},
                            },
                        },
                        "required": ["title"],
                    },
                },
                "featuredArtist": {
                    "type": "object",
                    "properties": {
                        "did": {"type": "string"},
                        "handle": {"type": "string"},
                    },
                    "required": ["did", "handle"],
                },
            },
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            (lexicon_dir / "track.json").write_text(json.dumps(lexicon))

            output_dir = Path(tmpdir) / "generated"
            files = generate(str(lexicon_dir), str(output_dir))

            content = Path(files[0]).read_text()

            # internal ref should resolve to class name, not dict
            assert "list[FmPlyrTrackFeaturedArtist]" in content
            assert "dict[str, Any]" not in content or "features" not in content

            # the referenced class should be generated
            assert "class FmPlyrTrackFeaturedArtist(BaseModel):" in content

    def test_external_ref_resolution(self):
        """external refs should resolve to class names."""
        from pmgfal import generate

        lexicon = {
            "lexicon": 1,
            "id": "fm.plyr.like",
            "defs": {
                "main": {
                    "type": "record",
                    "record": {
                        "type": "object",
                        "properties": {
                            "subject": {
                                "type": "ref",
                                "ref": "com.atproto.repo.strongRef",
                            },
                        },
                        "required": ["subject"],
                    },
                },
            },
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            (lexicon_dir / "like.json").write_text(json.dumps(lexicon))

            output_dir = Path(tmpdir) / "generated"
            files = generate(str(lexicon_dir), str(output_dir))

            content = Path(files[0]).read_text()

            # external ref should resolve to class name
            assert "subject: ComAtprotoRepoStrongRef" in content

            # builtin strongRef class should be generated
            assert "class ComAtprotoRepoStrongRef(BaseModel):" in content
            assert "uri: str" in content
            assert "cid: str" in content


class TestCaching:
    """test caching behavior."""

    def test_cache_hit(self):
        """second run should hit cache."""
        from pmgfal import main

        lexicon = {
            "lexicon": 1,
            "id": "test.cache",
            "defs": {
                "main": {
                    "type": "record",
                    "record": {
                        "type": "object",
                        "properties": {"x": {"type": "string"}},
                    },
                }
            },
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            (lexicon_dir / "cache.json").write_text(json.dumps(lexicon))

            output_dir = Path(tmpdir) / "generated"

            # first run - cache miss
            result = main([str(lexicon_dir), "-o", str(output_dir)])
            assert result == 0
            assert (output_dir / "models.py").exists()

            # delete output to prove cache works
            (output_dir / "models.py").unlink()

            # second run - cache hit
            result = main([str(lexicon_dir), "-o", str(output_dir)])
            assert result == 0
            assert (output_dir / "models.py").exists()

    def test_no_cache_flag(self):
        """--no-cache should force regeneration."""
        from pmgfal import main

        lexicon = {
            "lexicon": 1,
            "id": "test.nocache",
            "defs": {
                "main": {
                    "type": "record",
                    "record": {
                        "type": "object",
                        "properties": {"y": {"type": "string"}},
                    },
                }
            },
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            lexicon_dir = Path(tmpdir) / "lexicons"
            lexicon_dir.mkdir()
            (lexicon_dir / "nocache.json").write_text(json.dumps(lexicon))

            output_dir = Path(tmpdir) / "generated"

            # first run
            result = main([str(lexicon_dir), "-o", str(output_dir)])
            assert result == 0

            # second run with --no-cache
            result = main([str(lexicon_dir), "-o", str(output_dir), "--no-cache"])
            assert result == 0
