"""
Tests for the check-datetime-timezone pre-commit hook.

Verifies that the hook flags any ``Column(DateTime(...))`` /
``sa.Column(..., DateTime(...))`` pattern in both model files under
``src/local_deep_research/database/models/`` and migration files under
``src/local_deep_research/database/migrations/versions/``, and allows
``UtcDateTime`` when the ``sqlalchemy_utc`` import is present.

Notable regressions locked in here:

* No migration exemption for ``sa.DateTime(timezone=True)``
  (see ``test_no_migration_exemption_for_timezone_true``). This is the
  inversion of the superseded PR #3515 approach.
* ``ast.IfExp`` branches are BOTH inspected — a violation in either
  ``body`` or ``orelse`` is caught.
* Path filter anchors on ``src/local_deep_research/`` so
  ``tests/database/models/`` is NOT scanned.
"""

import subprocess
import sys
import tempfile
from pathlib import Path


HOOK_SCRIPT = (
    Path(__file__).parent.parent.parent
    / "scripts"
    / "pre_commit"
    / "check_datetime_timezone.py"
)


PACKAGE_PREFIX = ("src", "local_deep_research")


def _run_hook_on_source(
    source: str,
    subpath: tuple = PACKAGE_PREFIX + ("database", "models"),
    filename: str = "example.py",
) -> subprocess.CompletedProcess:
    """Write *source* into a directory tree matching *subpath* and run the hook."""
    with tempfile.TemporaryDirectory() as base:
        nested = Path(base).joinpath(*subpath)
        nested.mkdir(parents=True)
        target = nested / filename
        target.write_text(source)
        return subprocess.run(
            [sys.executable, str(HOOK_SCRIPT), str(target)],
            capture_output=True,
            text=True,
            timeout=30,
        )


def _run_hook_on_files(
    files: list,
) -> subprocess.CompletedProcess:
    """Run the hook against multiple prepared files.

    ``files`` is a list of ``(subpath_tuple, filename, source)`` triples.
    Returns after running the hook once with all file paths as arguments.
    """
    with tempfile.TemporaryDirectory() as base:
        paths = []
        for subpath, filename, source in files:
            nested = Path(base).joinpath(*subpath)
            nested.mkdir(parents=True, exist_ok=True)
            target = nested / filename
            target.write_text(source)
            paths.append(str(target))
        return subprocess.run(
            [sys.executable, str(HOOK_SCRIPT), *paths],
            capture_output=True,
            text=True,
            timeout=30,
        )


MODEL_PATH = PACKAGE_PREFIX + ("database", "models")
MIGRATION_PATH = PACKAGE_PREFIX + ("database", "migrations", "versions")
OUT_OF_SCOPE_PATH = ("tests", "database", "models")


IMPORT_LINE = "from sqlalchemy_utc import UtcDateTime\n"
IMPORT_LINE_COMBO = "from sqlalchemy_utc import utcnow, UtcDateTime\n"


# ---------------------------------------------------------------------------
# Flagged patterns
# ---------------------------------------------------------------------------


class TestFlagsViolations:
    """Patterns that must exit with returncode 1."""

    def test_model_bare_datetime_call_flagged(self):
        result = _run_hook_on_source(
            "ts = Column(DateTime(), nullable=False)\n",
            subpath=MODEL_PATH,
        )
        assert result.returncode == 1
        assert "UtcDateTime" in result.stdout

    def test_model_bare_datetime_name_flagged(self):
        """Bare ``Column(DateTime)`` without parens — new stricter handling."""
        result = _run_hook_on_source(
            "ts = Column(DateTime)\n",
            subpath=MODEL_PATH,
        )
        assert result.returncode == 1

    def test_model_utcdatetime_missing_import_flagged(self):
        result = _run_hook_on_source(
            "ts = Column(UtcDateTime, nullable=False)\n",
            subpath=MODEL_PATH,
        )
        assert result.returncode == 1
        assert "Missing import" in result.stdout

    def test_no_migration_exemption_for_timezone_true(self):
        """PR #3515's migration exemption is intentionally removed.

        A migration that uses ``sa.DateTime(timezone=True)`` is a
        violation under the ``UtcDateTime``-everywhere rule, even though
        the old hook behaviour accepted it.
        """
        result = _run_hook_on_source(
            'created_at = sa.Column("created_at", sa.DateTime(timezone=True), nullable=False)\n',
            subpath=MIGRATION_PATH,
            filename="0006_example.py",
        )
        assert result.returncode == 1
        assert "UtcDateTime" in result.stdout

    def test_migration_bare_datetime_flagged(self):
        result = _run_hook_on_source(
            'ts = sa.Column("ts", sa.DateTime())\n',
            subpath=MIGRATION_PATH,
            filename="0006_example.py",
        )
        assert result.returncode == 1

    def test_conditional_type_violation_in_body(self):
        """``Column(DateTime() if cond else UtcDateTime())`` — body branch."""
        source = IMPORT_LINE + (
            "ts = Column(DateTime() if flag else UtcDateTime())\n"
        )
        result = _run_hook_on_source(source, subpath=MODEL_PATH)
        assert result.returncode == 1

    def test_conditional_type_violation_in_orelse(self):
        """``Column(UtcDateTime() if cond else DateTime())`` — orelse branch.

        Regression guard for the asymmetric-branch bug: if the helper
        short-circuits on the first resolved branch, this case slips
        through silently.
        """
        source = IMPORT_LINE + (
            "ts = Column(UtcDateTime() if flag else DateTime())\n"
        )
        result = _run_hook_on_source(source, subpath=MODEL_PATH)
        assert result.returncode == 1

    def test_two_violations_in_one_file_both_reported(self):
        source = (
            "a = Column(DateTime(), nullable=False)\n"
            "b = Column(DateTime(), nullable=False)\n"
        )
        result = _run_hook_on_source(source, subpath=MODEL_PATH)
        assert result.returncode == 1
        assert "Line 1" in result.stdout
        assert "Line 2" in result.stdout

    def test_batch_only_dirty_file_reported(self):
        dirty = "ts = Column(DateTime(), nullable=False)\n"
        clean = IMPORT_LINE + "ts = Column(UtcDateTime, nullable=False)\n"
        result = _run_hook_on_files(
            [
                (MODEL_PATH, "dirty.py", dirty),
                (MODEL_PATH, "clean.py", clean),
            ]
        )
        assert result.returncode == 1
        assert "dirty.py" in result.stdout
        assert "clean.py" not in result.stdout

    def test_path_filter_scans_src_dir(self):
        """Positive side of the path-filter boundary."""
        result = _run_hook_on_source(
            "ts = Column(DateTime(), nullable=False)\n",
            subpath=MODEL_PATH,
        )
        assert result.returncode == 1


# ---------------------------------------------------------------------------
# Allowed patterns
# ---------------------------------------------------------------------------


class TestAllowsCorrectPatterns:
    """Patterns that must exit with returncode 0."""

    def test_model_utcdatetime_bare_with_import(self):
        source = IMPORT_LINE + (
            "ts = Column(UtcDateTime, default=utcnow(), nullable=False)\n"
        )
        result = _run_hook_on_source(source, subpath=MODEL_PATH)
        assert result.returncode == 0

    def test_migration_utcdatetime_call_with_import(self):
        source = IMPORT_LINE + (
            'ts = sa.Column("ts", UtcDateTime(), nullable=False)\n'
        )
        result = _run_hook_on_source(
            source, subpath=MIGRATION_PATH, filename="0006_example.py"
        )
        assert result.returncode == 0

    def test_combo_import_order_detected(self):
        """``from sqlalchemy_utc import utcnow, UtcDateTime`` is accepted."""
        source = IMPORT_LINE_COMBO + (
            "ts = Column(UtcDateTime, default=utcnow(), nullable=False)\n"
        )
        result = _run_hook_on_source(source, subpath=MODEL_PATH)
        assert result.returncode == 0

    def test_empty_file(self):
        result = _run_hook_on_source("", subpath=MODEL_PATH)
        assert result.returncode == 0

    def test_syntax_error_file_does_not_crash(self):
        """Hook's ast.parse swallows SyntaxError and returns 0 for that file."""
        result = _run_hook_on_source("def foo(:\n", subpath=MODEL_PATH)
        assert result.returncode == 0


# ---------------------------------------------------------------------------
# Path filter
# ---------------------------------------------------------------------------


class TestPathFilter:
    """The hook must only scan files under the package prefix."""

    def test_file_outside_both_dirs_is_skipped(self):
        """Arbitrary path — hook does not scan."""
        with tempfile.TemporaryDirectory() as base:
            target = Path(base) / "random" / "file.py"
            target.parent.mkdir(parents=True)
            target.write_text("ts = Column(DateTime(), nullable=False)\n")
            result = subprocess.run(
                [sys.executable, str(HOOK_SCRIPT), str(target)],
                capture_output=True,
                text=True,
                timeout=30,
            )
        assert result.returncode == 0

    def test_tests_dir_with_database_models_segment_is_skipped(self):
        """``tests/database/models/foo.py`` must NOT be scanned.

        Prevents the pre-existing substring fallback from false-firing
        on test fixtures that happen to declare schema-like code.
        """
        result = _run_hook_on_source(
            "ts = Column(DateTime(), nullable=False)\n",
            subpath=OUT_OF_SCOPE_PATH,
        )
        assert result.returncode == 0

    def test_models_backup_substring_boundary(self):
        """``database/models_backup/`` must NOT match."""
        result = _run_hook_on_source(
            "ts = Column(DateTime(), nullable=False)\n",
            subpath=PACKAGE_PREFIX + ("database", "models_backup"),
        )
        assert result.returncode == 0


# ---------------------------------------------------------------------------
# Output format
# ---------------------------------------------------------------------------


class TestOutputFormat:
    """Lock in the user-facing output so refactors don't silently change it."""

    def test_violation_stdout_includes_filename(self):
        result = _run_hook_on_source(
            "ts = Column(DateTime(), nullable=False)\n",
            subpath=MODEL_PATH,
            filename="my_model.py",
        )
        assert result.returncode == 1
        assert "my_model.py" in result.stdout

    def test_violation_stdout_includes_fix_hint(self):
        result = _run_hook_on_source(
            "ts = Column(DateTime(), nullable=False)\n",
            subpath=MODEL_PATH,
        )
        assert "UtcDateTime" in result.stdout
        assert "sqlalchemy_utc" in result.stdout
