"""
Tests for the check-settings-manager-thread-safety pre-commit hook.

Ensures the hook correctly detects get_settings_manager() calls without
db_session= in background-thread functions (identified by @thread_cleanup
decorator or _background_/_auto_/_worker naming), while allowing legitimate
usage in Flask request context and when db_session= is passed explicitly.
"""

import ast
import sys
from importlib import import_module
from pathlib import Path


HOOKS_DIR = Path(__file__).parent.parent.parent / ".pre-commit-hooks"
sys.path.insert(0, str(HOOKS_DIR))

hook_module = import_module("check-settings-manager-thread-safety")  # noqa: E402
SettingsManagerThreadSafetyChecker = (
    hook_module.SettingsManagerThreadSafetyChecker
)


def _check_code(code: str, filename: str = "src/module.py") -> list:
    """Parse code and return the checker's issues list (line_no, message)."""
    tree = ast.parse(code)
    checker = SettingsManagerThreadSafetyChecker(filename)
    checker.visit(tree)
    return checker.issues


class TestThreadFunctionDetection:
    """Detection tests — functions the hook should classify as background threads."""

    def test_detects_thread_cleanup_decorator(self):
        code = """
@thread_cleanup
def some_func():
    get_settings_manager(username="alice")
"""
        issues = _check_code(code)
        assert len(issues) == 1
        assert issues[0][0] == 4

    def test_detects_thread_cleanup_decorator_call_form(self):
        """@thread_cleanup() with parentheses should also trigger."""
        code = """
@thread_cleanup()
def some_func():
    get_settings_manager(username="alice")
"""
        issues = _check_code(code)
        assert len(issues) == 1

    def test_detects_module_qualified_thread_cleanup_decorator(self):
        """@module.thread_cleanup should be detected via ast.Attribute."""
        code = """
@db.thread_cleanup
def some_func():
    get_settings_manager(username="alice")
"""
        issues = _check_code(code)
        assert len(issues) == 1

    def test_detects_background_prefix(self):
        code = """
def _background_index_worker(username):
    get_settings_manager(username=username)
"""
        issues = _check_code(code)
        assert len(issues) == 1

    def test_detects_auto_prefix(self):
        code = """
def _auto_index_documents_worker(username):
    get_settings_manager(username=username)
"""
        issues = _check_code(code)
        assert len(issues) == 1

    def test_detects_worker_suffix(self):
        code = """
def my_worker(username):
    get_settings_manager(username=username)
"""
        issues = _check_code(code)
        assert len(issues) == 1

    def test_detects_async_thread_function(self):
        """async def _background_* should be visited via visit_AsyncFunctionDef."""
        code = """
async def _background_evaluation(queue):
    get_settings_manager(username="alice")
"""
        issues = _check_code(code)
        assert len(issues) == 1


class TestCallPatternDetection:
    """Tests for how the hook recognises get_settings_manager() call shapes."""

    def test_flags_bare_call_without_db_session(self):
        code = """
@thread_cleanup
def worker():
    get_settings_manager()
"""
        assert len(_check_code(code)) == 1

    def test_flags_username_only_call(self):
        code = """
@thread_cleanup
def worker():
    get_settings_manager(username="alice")
"""
        assert len(_check_code(code)) == 1

    def test_flags_attribute_style_call(self):
        """module.get_settings_manager() should be caught via ast.Attribute."""
        code = """
@thread_cleanup
def worker():
    db_utils.get_settings_manager(username="alice")
"""
        assert len(_check_code(code)) == 1


class TestAllowances:
    """Cases that should NOT be flagged."""

    def test_allows_db_session_kwarg(self):
        code = """
@thread_cleanup
def worker(db_session):
    get_settings_manager(db_session=db_session, username="alice")
"""
        assert _check_code(code) == []

    def test_allows_db_session_only(self):
        code = """
@thread_cleanup
def worker(db_session):
    get_settings_manager(db_session=db_session)
"""
        assert _check_code(code) == []

    def test_allows_positional_db_session(self):
        """db_session is the first positional param - a positional arg counts."""
        code = """
@thread_cleanup
def worker(db_session, username):
    get_settings_manager(db_session, username)
"""
        assert _check_code(code) == []

    def test_allows_positional_db_session_only(self):
        code = """
@thread_cleanup
def worker(db_session):
    get_settings_manager(db_session)
"""
        assert _check_code(code) == []

    def test_allows_non_thread_function(self):
        """Flask route / non-thread code may call get_settings_manager() freely."""
        code = """
def route_handler():
    get_settings_manager(username="alice")
"""
        assert _check_code(code) == []

    def test_allows_unrelated_call(self):
        """Calls to other functions should never be flagged."""
        code = """
@thread_cleanup
def worker():
    get_other_thing(username="alice")
"""
        assert _check_code(code) == []

    def test_allows_thread_function_without_call(self):
        """A thread function that never calls get_settings_manager is fine."""
        code = """
@thread_cleanup
def worker():
    do_something_else()
"""
        assert _check_code(code) == []


class TestMultipleAndPositional:
    """Multi-issue reporting and line-number correctness."""

    def test_reports_multiple_violations(self):
        code = """
@thread_cleanup
def worker_one():
    get_settings_manager(username="a")
    get_settings_manager(username="b")
"""
        issues = _check_code(code)
        assert len(issues) == 2

    def test_reports_correct_line_numbers(self):
        code = """
@thread_cleanup
def worker():
    x = 1
    get_settings_manager(username="a")
"""
        issues = _check_code(code)
        assert len(issues) == 1
        assert issues[0][0] == 5

    def test_mixed_safe_and_unsafe_in_same_function(self):
        code = """
@thread_cleanup
def worker(db_session):
    get_settings_manager(db_session=db_session)  # safe
    get_settings_manager(username="x")           # unsafe
"""
        issues = _check_code(code)
        assert len(issues) == 1
        assert issues[0][0] == 5


class TestCheckFileIntegration:
    """check_file() integration tests using tmp_path."""

    def test_clean_file_passes(self, tmp_path):
        clean = tmp_path / "clean.py"
        clean.write_text("""
@thread_cleanup
def worker(db_session):
    get_settings_manager(db_session=db_session, username="x")
""")
        assert hook_module.check_file(clean) == []

    def test_violation_file_fails(self, tmp_path):
        bad = tmp_path / "bad.py"
        bad.write_text("""
@thread_cleanup
def worker():
    get_settings_manager(username="x")
""")
        issues = hook_module.check_file(bad)
        assert len(issues) == 1
        filepath, lineno, _msg = issues[0]
        assert filepath == str(bad)
        assert lineno == 4

    def test_syntax_error_is_tolerated(self, tmp_path, capsys):
        """Files with syntax errors should not raise; they print to stderr."""
        broken = tmp_path / "broken.py"
        broken.write_text("def bad(:\n    pass")
        issues = hook_module.check_file(broken)
        assert issues == []
        captured = capsys.readouterr()
        assert "Syntax error" in captured.err

    def test_non_python_file_via_main_is_skipped(self, tmp_path, monkeypatch):
        """main() should skip non-.py files without inspecting them."""
        non_py = tmp_path / "README.md"
        non_py.write_text("not python @thread_cleanup def worker():")
        monkeypatch.setattr(sys, "argv", ["hook", str(non_py)])
        assert hook_module.main() == 0

    def test_main_returns_zero_on_clean(self, tmp_path, monkeypatch):
        clean = tmp_path / "clean.py"
        clean.write_text("""
def route():
    get_settings_manager(username="x")
""")
        monkeypatch.setattr(sys, "argv", ["hook", str(clean)])
        assert hook_module.main() == 0

    def test_main_returns_one_on_violation(self, tmp_path, monkeypatch, capsys):
        bad = tmp_path / "bad.py"
        bad.write_text("""
@thread_cleanup
def worker():
    get_settings_manager(username="x")
""")
        monkeypatch.setattr(sys, "argv", ["hook", str(bad)])
        assert hook_module.main() == 1
        captured = capsys.readouterr()
        assert "get_settings_manager()" in captured.out
        assert "get_user_db_session" in captured.out

    def test_main_with_no_files_returns_zero(self, monkeypatch):
        monkeypatch.setattr(sys, "argv", ["hook"])
        assert hook_module.main() == 0


class TestNestedFunctions:
    """Tests for the nested-function walker that prevents double-reporting."""

    def test_nested_thread_in_thread_reports_once(self):
        """Unsafe call inside a nested thread fn must be reported exactly once,
        not twice (once from the outer walk + once from the inner visit)."""
        code = """
@thread_cleanup
def outer_worker(db_session):
    get_settings_manager(db_session)  # safe

    @thread_cleanup
    def inner_worker():
        get_settings_manager(username="x")  # unsafe, reported once
"""
        issues = _check_code(code)
        assert len(issues) == 1

    def test_nested_non_thread_function_not_checked(self):
        """An unsafe call inside a nested non-thread fn should not be flagged
        just because the outer function is a thread fn."""
        code = """
@thread_cleanup
def outer_worker():
    def inner_helper():
        get_settings_manager(username="x")  # NOT flagged: inner is not a thread fn
"""
        assert _check_code(code) == []

    def test_lambda_inside_thread_not_checked(self):
        """A lambda body inside a thread fn is not walked (lambdas have no name
        to classify)."""
        code = """
@thread_cleanup
def outer_worker():
    cb = lambda: get_settings_manager(username="x")
"""
        assert _check_code(code) == []


class TestThreadFunctionClassifier:
    """Unit tests for SettingsManagerThreadSafetyChecker._is_thread_function."""

    def _classify(self, code: str) -> bool:
        tree = ast.parse(code)
        func = tree.body[0]
        checker = SettingsManagerThreadSafetyChecker("x.py")
        return checker._is_thread_function(func)

    def test_plain_function_is_not_thread(self):
        assert self._classify("def foo(): pass") is False

    def test_background_prefix_is_thread(self):
        assert self._classify("def _background_x(): pass") is True

    def test_auto_prefix_is_thread(self):
        assert self._classify("def _auto_x(): pass") is True

    def test_worker_suffix_is_thread(self):
        assert self._classify("def foo_worker(): pass") is True

    def test_decorator_is_thread(self):
        code = "@thread_cleanup\ndef foo(): pass"
        assert self._classify(code) is True

    def test_unrelated_decorator_is_not_thread(self):
        code = "@staticmethod\ndef foo(): pass"
        assert self._classify(code) is False
