import pytest
import json
import tempfile
from pathlib import Path
from textwrap import dedent

import skylos
from skylos.analyzer import Skylos
from skylos.constants import DEFAULT_EXCLUDE_FOLDERS


class TestSkylosIntegration:
    @pytest.fixture
    def temp_project(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            project_path = Path(temp_dir)

            pyproject = project_path / "pyproject.toml"
            pyproject.write_text("[tool.skylos]\n")

            main_py = project_path / "main.py"
            main_py.write_text(
                dedent("""
                import os  # unused import
                import sys
                from typing import List  # unused import
                from collections import defaultdict
                from utils import UsedClass, exported_function
                
                def used_function(x):
                    '''This function is called and should not be flagged'''
                    return x * 2
                
                def unused_function(a, b):
                    '''This function is never called'''
                    unused_var = a + b  # unused variable
                    return unused_var
                
                def main():
                    result = used_function(5)
                    print(result, file=sys.stderr)
                    data = defaultdict(list)
                    
                    obj = UsedClass()
                    obj.get_value()
                    exported_function()
                    
                    return data
                
                if __name__ == "__main__":
                    main()
            """)
            )

            utils_py = project_path / "utils.py"
            utils_py.write_text(
                dedent("""
                import json  # unused import
                
                class UsedClass:
                    '''This class is imported and used'''
                    def __init__(self):
                        self.value = 42
                    
                    def get_value(self):
                        return self.value
                
                class UnusedClass:
                    '''This class is never used'''
                    def __init__(self):
                        self.data = {}
                    
                    def unused_method(self, param):  # unused parameter
                        return "never called"
                
                def exported_function():
                    '''This function is used by main.py'''
                    return "exported"
            """)
            )

            package_dir = project_path / "mypackage"
            package_dir.mkdir()

            init_py = package_dir / "__init__.py"
            init_py.write_text(
                dedent("""
                from .submodule import PublicClass
                
                __all__ = ['PublicClass', 'public_function']
                
                def public_function():
                    return "public"
                
                def _private_function():
                    return "private"
            """)
            )

            sub_py = package_dir / "submodule.py"
            sub_py.write_text(
                dedent("""
                class PublicClass:
                    '''Exported via __init__.py'''
                    def method(self):
                        return "method"
                
                class InternalClass:
                    '''Not exported, should be flagged'''
                    pass
            """)
            )

            test_py = project_path / "test_example.py"
            test_py.write_text(
                dedent("""
                import unittest
                from main import used_function
                
                class TestExample(unittest.TestCase):
                    def test_used_function(self):
                        result = used_function(3)
                        self.assertEqual(result, 6)
                    
                    def test_unused_method(self):
                        pass
            """)
            )

            yield project_path

    def test_basic_analysis(self, temp_project):
        result_json = skylos.analyze(
            str(temp_project), exclude_folders=list(DEFAULT_EXCLUDE_FOLDERS)
        )
        result = json.loads(result_json)
        print("\n=== DEBUG ===")
        print("Unused functions:", [f["name"] for f in result["unused_functions"]])
        print("Unused imports:", [f["name"] for f in result["unused_imports"]])
        print("Whitelisted:", result.get("whitelisted", []))
        print("All definitions:", list(result.get("definitions", {}).keys())[:20])
        print("END OF DEBUG=============\n")

        assert "unused_functions" in result
        assert "unused_imports" in result
        assert "unused_variables" in result
        assert "unused_parameters" in result
        assert "unused_classes" in result

        assert len(result["unused_functions"]) > 0
        assert len(result["unused_imports"]) > 0

        unused_function_names = [f["name"] for f in result["unused_functions"]]
        assert any("unused_function" in name for name in unused_function_names), (
            f"Expected unused_function in {unused_function_names}"
        )

        assert "used_function" not in unused_function_names
        assert "main" not in unused_function_names

    def test_unused_imports_detection(self, temp_project):
        result_json = skylos.analyze(str(temp_project))
        result = json.loads(result_json)

        unused_imports = result["unused_imports"]
        import_names = [imp["name"] for imp in unused_imports]

        assert any("os" in name for name in import_names)
        assert any("typing.List" in name or "List" in name for name in import_names)

        used_imports = ["sys", "defaultdict"]
        for used_import in used_imports:
            assert not any(used_import in name for name in import_names)

    def test_class_detection(self, temp_project):
        result_json = skylos.analyze(str(temp_project))
        result = json.loads(result_json)

        unused_classes = result["unused_classes"]
        class_names = [cls["name"] for cls in unused_classes]

        assert any("UnusedClass" in name for name in class_names), (
            f"UnusedClass not found in {class_names}"
        )

        used_classes_flagged = [name for name in class_names if "UsedClass" in name]
        assert len(used_classes_flagged) == 0, (
            f"UsedClass was incorrectly flagged as unused: {used_classes_flagged}"
        )

        public_classes_flagged = [name for name in class_names if "PublicClass" in name]
        assert len(public_classes_flagged) == 0, (
            f"PublicClass was incorrectly flagged as unused: {public_classes_flagged}"
        )

    def test_exclude_folders(self, temp_project):
        result1 = skylos.analyze(str(temp_project))

        result2 = skylos.analyze(str(temp_project), exclude_folders=["mypackage"])

        data1 = json.loads(result1)
        data2 = json.loads(result2)

        total1 = sum(len(items) for items in data1.values() if isinstance(items, list))
        total2 = sum(len(items) for items in data2.values() if isinstance(items, list))

        assert total2 <= total1

    def test_custom_exclude_folders(self, temp_project):
        custom_dir = temp_project / "custom_exclude"
        custom_dir.mkdir()

        custom_file = custom_dir / "custom.py"
        custom_file.write_text(
            dedent("""
            def custom_function():
                return "should be excluded"
        """)
        )

        result_json = skylos.analyze(
            str(temp_project), exclude_folders=["custom_exclude"]
        )
        result = json.loads(result_json)

        all_files = []
        for category in [
            "unused_functions",
            "unused_imports",
            "unused_classes",
            "unused_variables",
        ]:
            for item in result[category]:
                all_files.append(item["file"])

        excluded_files = [f for f in all_files if "custom_exclude" in f]
        assert len(excluded_files) == 0, f"Found excluded files: {excluded_files}"

    def test_standalone_dirs_suppress_reachability_only(self, temp_project):
        tests_dir = temp_project / "tests"
        tests_dir.mkdir()
        (tests_dir / "test_api.py").write_text(
            dedent("""
            import math

            def helper():
                return 1

            class HelperSuite:
                pass
        """)
        )

        examples_dir = temp_project / "examples"
        examples_dir.mkdir()
        (examples_dir / "demo.py").write_text(
            dedent("""
            def example_main():
                return "demo"
        """)
        )

        benchmarks_dir = temp_project / "benchmarks"
        benchmarks_dir.mkdir()
        (benchmarks_dir / "bench_api.py").write_text(
            dedent("""
            class BenchmarkSuite:
                pass

            def time_parse():
                return 1
        """)
        )

        (temp_project / "test_example.py").write_text(
            dedent("""
            def root_test_helper():
                return 1
        """)
        )

        (temp_project / "conftest.py").write_text(
            dedent("""
            def root_conftest_helper():
                return 1
        """)
        )

        result = json.loads(skylos.analyze(str(temp_project)))

        unused_functions = {item["name"] for item in result["unused_functions"]}
        unused_classes = {item["name"] for item in result["unused_classes"]}
        unused_imports = {item["name"] for item in result["unused_imports"]}

        assert "helper" not in unused_functions
        assert "example_main" not in unused_functions
        assert "time_parse" not in unused_functions
        assert "root_test_helper" not in unused_functions
        assert "root_conftest_helper" not in unused_functions
        assert "HelperSuite" not in unused_classes
        assert "BenchmarkSuite" not in unused_classes
        assert "math" not in unused_imports

        suppressed_reasons = {
            item["reason"]
            for item in result.get("whitelisted", [])
            if "tests" in Path(item["file"]).parts
        }
        assert "test-only path" in suppressed_reasons

    def test_transitive_export_propagation(self, temp_project):
        pkg = temp_project / "pkg"
        pkg.mkdir()
        (pkg / "__init__.py").write_text("from pkg.events import Events\n")
        (pkg / "events.py").write_text(
            dedent("""
            class EventHook:
                def add_listener(self): pass
                def remove_listener(self): pass
                def _internal(self): pass

            class Events:
                request: EventHook
                response: EventHook
        """)
        )
        (temp_project / "app.py").write_text(
            dedent("""
            from pkg import Events

            e = Events()
            e.request.add_listener()
        """)
        )

        result = json.loads(skylos.analyze(str(temp_project)))
        unused_functions = {item["name"] for item in result["unused_functions"]}
        unused_classes = {item["name"] for item in result["unused_classes"]}

        assert "EventHook" not in unused_classes
        assert "remove_listener" not in unused_functions
        assert "add_listener" not in unused_functions
        assert any("_internal" in name for name in unused_functions)

    def test_magic_methods_excluded(self, temp_project):
        magic_py = temp_project / "magic.py"
        magic_py.write_text(
            dedent("""
            class MagicClass:
                def __init__(self):
                    self.value = 0
                
                def __str__(self):
                    return str(self.value)
                
                def __len__(self):
                    return self.value
                
                def regular_method(self):
                    return "regular"
        """)
        )

        result_json = skylos.analyze(str(temp_project))
        result = json.loads(result_json)

        unused_functions = result["unused_functions"]
        function_names = [f["name"] for f in unused_functions]

        magic_methods = ["__init__", "__str__", "__len__"]
        for magic_method in magic_methods:
            assert not any(magic_method in name for name in function_names)

    def test_test_methods_excluded(self, temp_project):
        """Test that test methods are not flagged as unused"""
        result_json = skylos.analyze(str(temp_project))
        result = json.loads(result_json)

        unused_functions = result["unused_functions"]
        function_names = [f["name"] for f in unused_functions]

        test_methods = ["test_used_function", "test_unused_method"]
        for test_method in test_methods:
            assert not any(test_method in name for name in function_names)

    def test_exported_functions_excluded(self, temp_project):
        result_json = skylos.analyze(str(temp_project))
        result = json.loads(result_json)

        unused_functions = result["unused_functions"]
        function_names = [f["name"] for f in unused_functions]

        assert not any("public_function" in name for name in function_names)

    def test_single_file_analysis(self, temp_project):
        main_file = temp_project / "main.py"

        result_json = skylos.analyze(str(main_file))
        result = json.loads(result_json)

        assert len(result["unused_functions"]) > 0
        assert len(result["unused_imports"]) > 0

        all_files = set()
        for category in ["unused_functions", "unused_imports", "unused_variables"]:
            for item in result[category]:
                all_files.add(Path(item["file"]).name)

        assert "main.py" in all_files
        assert "utils.py" not in all_files

    def test_confidence_levels(self, temp_project):
        result_json = skylos.analyze(str(temp_project))
        result = json.loads(result_json)

        for category in ["unused_functions", "unused_imports", "unused_variables"]:
            for item in result[category]:
                assert "confidence" in item
                assert isinstance(item["confidence"], (int, float))
                assert 0 <= item["confidence"] <= 100

    def test_analyzer_class_direct(self, temp_project):
        analyzer = Skylos()
        result_json = analyzer.analyze(str(temp_project), thr=50)
        result = json.loads(result_json)

        expected_keys = [
            "unused_functions",
            "unused_imports",
            "unused_classes",
            "unused_variables",
            "unused_parameters",
            "analysis_summary",
        ]

        for key in expected_keys:
            assert key in result

    def test_empty_project(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            result_json = skylos.analyze(temp_dir)
            result = json.loads(result_json)

            assert result["unused_functions"] == []
            assert result["unused_imports"] == []
            assert result["analysis_summary"]["total_files"] == 0

    def test_threshold_filtering(self, temp_project):
        result_high = json.loads(skylos.analyze(str(temp_project), conf=95))

        result_low = json.loads(skylos.analyze(str(temp_project), conf=10))

        assert len(result_high["unused_functions"]) <= len(
            result_low["unused_functions"]
        )
        assert len(result_high["unused_imports"]) <= len(result_low["unused_imports"])


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
