import tempfile
from contextlib import contextmanager
from pathlib import Path
from unittest import TestCase
from unittest.mock import patch

from _pytest import pathlib as pytest_pathlib

from pytest_oduit import (
    _extract_addon_name,
    _find_manifest_path,
    disable_odoo_test_retry,
    get_odoo_version,
    monkey_patch_resolve_pkg_root_and_module_name,
    support_subtest,
)


class TestPytestOduit(TestCase):
    @contextmanager
    def fake_module(self, with_manifest=True, using_addons_namespace=False):
        directory = tempfile.TemporaryDirectory()
        try:
            module_path = Path(directory.name)
            files = []
            if using_addons_namespace:
                files.append(module_path / "odoo" / "__init__.py")
                files.append(module_path / "odoo" / "addons" / "__init__.py")
                module_path = module_path / "odoo" / "addons" / "my_module"
                module_path.mkdir(parents=True, exist_ok=True)
            manifest_path = None
            if with_manifest:
                manifest_path = module_path / "__manifest__.py"
                files.append(manifest_path)
            test_path = module_path / "tests" / "test_module.py"
            test_path.parent.mkdir(parents=True, exist_ok=True)
            files.append(test_path)
            files.append(module_path / "__init__.py")
            files.append(module_path / "tests" / "__init__.py")
            for file_path in files:
                file_path.touch()
            yield (
                module_path,
                manifest_path,
                test_path,
            )
        finally:
            directory.cleanup()

    def test_find_manifest_path_less_than_5_directories(self):
        self.assertIsNone(_find_manifest_path(Path("/some/path")))

    def test_find_manifest_path_from_test_module(self):
        with self.fake_module() as (_, manifest_path, test_path):
            self.assertEqual(_find_manifest_path(test_path), manifest_path)

    def test_find_manifest_path_from_itself(self):
        with self.fake_module() as (_, manifest_path, _):
            self.assertEqual(_find_manifest_path(manifest_path), manifest_path)

    def test_find_manifest_path_from_brother(self):
        with self.fake_module() as (module_path, manifest_path, _):
            test = module_path / "test_something.py"
            test.touch()
            self.assertEqual(_find_manifest_path(test), manifest_path)

    def test_resolve_pkg_root_and_module_name(self):
        monkey_patch_resolve_pkg_root_and_module_name()
        with self.fake_module() as (module_path, _, test_path):
            pkg_root, module_name = pytest_pathlib.resolve_pkg_root_and_module_name(
                test_path
            )
            self.assertEqual(
                module_name, f"odoo.addons.{module_path.name}.tests.test_module"
            )

    def test_resolve_pkg_root_and_module_name_not_odoo_module(self):
        monkey_patch_resolve_pkg_root_and_module_name()

        with self.fake_module(with_manifest=False) as (module_path, _, test_path):
            pkg_root, module_name = pytest_pathlib.resolve_pkg_root_and_module_name(
                test_path
            )
            self.assertEqual(module_name, f"{module_path.name}.tests.test_module")

    def test_resolve_pkg_root_and_module_name_namespace_ok(self):
        monkey_patch_resolve_pkg_root_and_module_name()

        with self.fake_module(with_manifest=True, using_addons_namespace=True) as (
            module_path,
            _,
            test_path,
        ):
            pkg_root, module_name = pytest_pathlib.resolve_pkg_root_and_module_name(
                test_path
            )
            self.assertEqual(module_name, "odoo.addons.my_module.tests.test_module")

    def test_disable_odoo_test_retry(self):
        from odoo.tests import BaseCase

        original_basecase_run = BaseCase.run

        def restore_basecase_run():
            BaseCase.run = original_basecase_run

        self.addCleanup(restore_basecase_run)

        disable_odoo_test_retry()
        with patch("odoo.tests.BaseCase._call_something") as mock:
            BaseCase().run()
            mock.assert_not_called()

    def test_disable_odoo_test_retry_ignore_run_doesnt_exists(self):
        from odoo.tests import BaseCase

        original_basecase_run = BaseCase.run

        def restore_basecase_run():
            BaseCase.run = original_basecase_run

        self.addCleanup(restore_basecase_run)

        del BaseCase.run

        disable_odoo_test_retry()

        with patch("odoo.tests.BaseCase._call_something") as mock:
            BaseCase().run()
            mock.assert_not_called()

    def test_import_error(self):
        from odoo import tests

        original_BaseCase = tests.BaseCase

        def restore_basecase():
            tests.BaseCase = original_BaseCase

        self.addCleanup(restore_basecase)
        del tests.BaseCase
        disable_odoo_test_retry()

    def test_support_subtest(self):
        from odoo.tests import case

        original_test_case = case.TestCase

        def restore():
            case.TestCase = original_test_case

        self.addCleanup(restore)
        support_subtest()

        from odoo.tests import BaseCase
        from odoo.tests.case import TestCase as OdooTestCase

        self.assertTrue(OdooTestCase.subTest is TestCase.subTest)
        self.assertTrue(BaseCase.subTest is TestCase.subTest)
        self.assertTrue(OdooTestCase.run is TestCase.run)

    def test_support_subtest_import_error(self):
        from odoo.tests import case

        original_odoo_test_case = case.TestCase

        def restore_testcase():
            case.TestCase = original_odoo_test_case

        self.addCleanup(restore_testcase)

        del case.TestCase
        support_subtest()


class TestExtractAddonName(TestCase):
    @contextmanager
    def create_addon_structure(self, addon_name="test_addon"):
        """Create a temporary addon structure with __manifest__.py file."""
        directory = tempfile.TemporaryDirectory()
        try:
            addon_path = Path(directory.name) / "addons" / addon_name
            addon_path.mkdir(parents=True, exist_ok=True)

            # Create __manifest__.py
            manifest_path = addon_path / "__manifest__.py"
            manifest_path.write_text("{'name': 'Test Addon', 'installable': True}")

            # Create tests directory
            tests_path = addon_path / "tests"
            tests_path.mkdir(exist_ok=True)

            # Create test file
            test_file = tests_path / "test_something.py"
            test_file.touch()

            yield {
                "addon_path": addon_path,
                "test_file": test_file,
                "tests_dir": tests_path,
                "manifest": manifest_path,
            }
        finally:
            directory.cleanup()

    def test_extract_addon_name_from_addon_directory(self):
        """Test extracting addon name when path is the addon directory itself."""
        with self.create_addon_structure("sale") as paths:
            addon_name = _extract_addon_name(paths["addon_path"])
            self.assertEqual(addon_name, "sale")

    def test_extract_addon_name_from_test_file(self):
        """Test extracting addon name from a test file path."""
        with self.create_addon_structure("purchase") as paths:
            addon_name = _extract_addon_name(paths["test_file"])
            self.assertEqual(addon_name, "purchase")

    def test_extract_addon_name_from_tests_directory(self):
        """Test extracting addon name from tests directory."""
        with self.create_addon_structure("stock") as paths:
            addon_name = _extract_addon_name(paths["tests_dir"])
            self.assertEqual(addon_name, "stock")

    def test_extract_addon_name_with_pytest_node_id(self):
        """Test extracting addon name from pytest node ID (path with ::)."""
        with self.create_addon_structure("crm") as paths:
            # Simulate pytest node ID like: path/to/test.py::TestClass::test_method
            # We need to test the logic that splits on '::'
            # The actual split happens in pytest_cmdline_main,
            # but we can test the extraction
            addon_name = _extract_addon_name(paths["test_file"])
            self.assertEqual(addon_name, "crm")

    def test_extract_addon_name_no_manifest(self):
        """Test that None is returned when no __manifest__.py exists."""
        with tempfile.TemporaryDirectory() as temp_dir:
            some_path = Path(temp_dir) / "random" / "path"
            some_path.mkdir(parents=True, exist_ok=True)
            addon_name = _extract_addon_name(some_path)
            self.assertIsNone(addon_name)

    def test_extract_addon_name_multiple_levels(self):
        """Test extracting addon name from deeply nested path."""
        with self.create_addon_structure("account") as paths:
            # Create a deeply nested file
            deep_path = paths["tests_dir"] / "subfolder" / "deep" / "test_deep.py"
            deep_path.parent.mkdir(parents=True, exist_ok=True)
            deep_path.touch()

            addon_name = _extract_addon_name(deep_path)
            self.assertEqual(addon_name, "account")


class TestGetOdooVersion(TestCase):
    def test_get_odoo_version_returns_tuple(self):
        """Test that get_odoo_version returns a tuple."""
        version = get_odoo_version()
        self.assertIsInstance(version, tuple)
        self.assertGreaterEqual(len(version), 2)

    def test_get_odoo_version_with_version_info(self):
        """Test get_odoo_version when odoo.release.version_info exists."""
        import odoo

        if hasattr(odoo, "release") and hasattr(odoo.release, "version_info"):
            version = get_odoo_version()
            self.assertEqual(version, odoo.release.version_info)
        else:
            self.skipTest("odoo.release.version_info not available in test environment")

    def test_get_odoo_version_without_version_info(self):
        """Test get_odoo_version when odoo.release.version_info doesn't exist."""
        import odoo

        if not hasattr(odoo, "release"):
            version = get_odoo_version()
            self.assertEqual(version, (999, 0, 0, "final", 0, ""))
            return

        original_version_info = None
        has_version_info = hasattr(odoo.release, "version_info")
        if has_version_info:
            original_version_info = odoo.release.version_info

        def restore_version_info():
            if has_version_info:
                odoo.release.version_info = original_version_info

        self.addCleanup(restore_version_info)

        if has_version_info:
            del odoo.release.version_info

        version = get_odoo_version()
        self.assertEqual(version, (999, 0, 0, "final", 0, ""))

    def test_get_odoo_version_comparison(self):
        """Test that get_odoo_version result can be compared with tuples."""
        version = get_odoo_version()

        self.assertTrue(version < (1000,))
        self.assertTrue(version >= (0,))

        self.assertIsInstance(version[0], int)
