import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

from generate_surface import (
    check_test_parity,
    extract_tests,
    generate_tests,
    mask_csharp_comments,
    reviewed_tests,
)


class ParityGenerationTests(unittest.TestCase):
    def test_reviewed_tests_include_owning_crate_unit_tests(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            root = Path(temporary)
            rust_file = root / "crates" / "example" / "src" / "internal_semantics.rs"
            rust_file.parent.mkdir(parents=True)
            case_id = "Example.Tests.cs::ExampleTests.InternalMember::test"
            rust_file.write_text(
                f"// parity-case: {case_id} {'a' * 64} translated\n"
                "#[test]\nfn internal_member() {\n"
                "    let fixture: &'static str = r#\"{ fixture }\"#;\n"
                "    assert_eq!(fixture, \"{ fixture }\");\n"
                "    Example::internal_member().expect(\"internal member\");\n"
                "}\n"
            )

            review = reviewed_tests(root)[case_id]
            self.assertEqual(review["rust_file"], "crates/example/src/internal_semantics.rs")
            self.assertEqual(review["rust_test"], "internal_member")
            self.assertRegex(str(review["rust_body_sha256"]), r"^[0-9a-f]{64}$")

    def test_comment_masking_preserves_offsets_and_ignores_fake_tests(self) -> None:
        source = '''var url = "https://example.com/[Test]"; // [Test]\n/* [TestCase(1)] */\n[Test]\n'''
        masked = mask_csharp_comments(source)
        self.assertEqual(len(masked), len(source))
        self.assertEqual(masked.count("\n"), source.count("\n"))
        self.assertIn('"https://example.com/[Test]"', masked)
        self.assertEqual(masked.count("[Test]"), 2)

        with tempfile.TemporaryDirectory() as temporary:
            upstream = Path(temporary)
            tests = upstream / "LibreMetaverse.Tests"
            tests.mkdir()
            (upstream / "LibreMetaverse.Rendering.Tests").mkdir()
            (tests / "Comments.cs").write_text(
                """public class Comments
{
    // [Test]
    // public void CommentedOut() {}
    [Test]
    [TestCase(1)]
    [TestCase(2)]
    public void Cases(int value) {}
}
"""
            )
            self.assertEqual([case["parameter_case"] for case in extract_tests(upstream)], ["1", "2"])

    def test_reviewed_rust_test_is_preserved_and_pending_cases_are_not_fake_tests(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            root = Path(temporary)
            upstream = root / "upstream"
            source = upstream / "LibreMetaverse.Tests" / "ExampleTests.cs"
            source.parent.mkdir(parents=True)
            (upstream / "LibreMetaverse.Rendering.Tests").mkdir()
            source.write_text(
                """namespace LibreMetaverse.Tests;
[Category("Example")]
public class ExampleTests
{
    [TestCase(1, Description = "one")]
    public void KeepsIdentity(int value) { Assert.That(value, Is.EqualTo(1)); }
    [Test]
    public void RemainsPending() { Assert.Pass(); }
}
"""
            )
            reviewed, _pending = extract_tests(upstream)
            output = root / "output"
            rust_file = output / "tests" / "compat" / "tests" / "example.rs"
            rust_file.parent.mkdir(parents=True)
            rust_file.write_text(
                f"// parity-case: {reviewed['id']} {reviewed['body_sha256']} translated\n"
                "#[test]\nfn translated_case() {\n"
                "    let actual = Example::keeps_identity(1).expect(\"KeepsIdentity\");\n"
                "    assert_eq!(actual, 1);\n"
                "}\n"
            )
            original = rust_file.read_bytes()

            with patch("generate_surface.EXPECTED_TESTS", 2):
                self.assertEqual(generate_tests(upstream, output), 2)
                self.assertEqual(rust_file.read_bytes(), original)
                generated = output / "tests/compat/tests/generated_parity.rs"
                self.assertNotIn(reviewed["id"], generated.read_text())
                self.assertNotIn("#[test]", generated.read_text())
                self.assertNotIn("pending(", generated.read_text())
                catalog = json.loads((output / "tests/upstream-tests.json").read_text())
                self.assertEqual(catalog["tests"][0]["status"], "translated")
                self.assertEqual(catalog["tests"][0]["rust_test"], "translated_case")
                self.assertEqual(check_test_parity(output)["unreviewed"], 1)

                rust_file.unlink()
                with self.assertRaisesRegex(RuntimeError, "Missing Rust parity cases"):
                    check_test_parity(output)

    def test_placeholder_and_dispatcher_bodies_cannot_count_as_reviewed(self) -> None:
        bodies = [
            "",
            '    pending("case");\n',
            '    assert_reviewed_case("Source.cs", "Method");\n',
            '    libremetaverse_types::unimplemented_api!("member");\n',
        ]
        for body in bodies:
            with self.subTest(body=body), tempfile.TemporaryDirectory() as temporary:
                root = Path(temporary)
                rust_file = root / "tests" / "compat" / "tests" / "invalid.rs"
                rust_file.parent.mkdir(parents=True)
                rust_file.write_text(
                    f"// parity-case: Example.cs::Example.Invalid::test {'a' * 64} translated\n"
                    f"#[test]\nfn invalid() {{\n{body}}}\n"
                )
                with self.assertRaisesRegex(RuntimeError, "empty body|forbidden placeholder/dispatcher"):
                    reviewed_tests(root)


if __name__ == "__main__":
    unittest.main()
