"""Step definitions for ADR compliance checker script coverage.

Covers every line and branch in ``scripts/check-adr-compliance.py``:
- check_adr002_async_model
- check_adr003_dependency_injection
- check_adr007_repository_pattern
- main()
"""

from __future__ import annotations

import importlib.util
import os
import shutil
import tempfile
from io import StringIO
from pathlib import Path
from unittest.mock import patch

from behave import given, then, when

# ---------------------------------------------------------------------------
# Module loader - the script uses hyphens so we need importlib
# ---------------------------------------------------------------------------


def _load_adr_module():
    spec = importlib.util.spec_from_file_location(
        "check_adr_compliance", "scripts/check-adr-compliance.py"
    )
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


_adr = _load_adr_module()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_tmp_src(context) -> Path:
    """Create a fresh temp directory and return a ``src_dir`` Path inside it."""
    context.tmp_root = tempfile.mkdtemp()
    src_dir = Path(context.tmp_root) / "src"
    src_dir.mkdir()
    context.src_dir = src_dir
    return src_dir


def _cleanup(context):
    """Remove the temp tree if it exists."""
    root = getattr(context, "tmp_root", None)
    if root and os.path.isdir(root):
        shutil.rmtree(root)


# ---------------------------------------------------------------------------
# Givens - ADR-002
# ---------------------------------------------------------------------------


@given("a temporary source directory with no application subdirectory")
def given_no_app_dir(context):
    _make_tmp_src(context)
    # deliberately do NOT create an "application" subdirectory


@given("a temporary source directory with a clean application module")
def given_clean_app_module(context):
    src = _make_tmp_src(context)
    app_dir = src / "application"
    app_dir.mkdir(parents=True)
    (app_dir / "clean.py").write_text("import asyncio\n")


@given(
    "a temporary source directory with an application module importing threading directly"
)
def given_app_import_threading(context):
    src = _make_tmp_src(context)
    app_dir = src / "application"
    app_dir.mkdir(parents=True)
    (app_dir / "bad.py").write_text("import threading\n")


@given(
    "a temporary source directory with an application module using from-threading-import"
)
def given_app_from_threading(context):
    src = _make_tmp_src(context)
    app_dir = src / "application"
    app_dir.mkdir(parents=True)
    (app_dir / "bad.py").write_text("from threading import Thread\n")


# ---------------------------------------------------------------------------
# Givens - ADR-003
# ---------------------------------------------------------------------------


@given("a temporary source directory with no services subdirectory")
def given_no_services_dir(context):
    src = _make_tmp_src(context)
    app_dir = src / "application"
    app_dir.mkdir(parents=True)
    # no "services" subdirectory


@given("a temporary source directory with only an __init__.py in services")
def given_dunder_init_only(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "__init__.py").write_text("")


@given(
    "a temporary source directory with a syntactically invalid Python file in services"
)
def given_syntax_error_file(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "broken.py").write_text("def foo(\n")


@given("a temporary source directory with a service class whose init takes only self")
def given_class_init_no_deps(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "my_service.py").write_text(
        "class MyService:\n    def __init__(self):\n        pass\n"
    )


@given(
    "a temporary source directory with a service class whose init takes self and a dependency"
)
def given_class_init_with_dep(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "good_service.py").write_text(
        "class GoodService:\n    def __init__(self, repo):\n        self.repo = repo\n"
    )


@given("a temporary source directory with a service class that has no init method")
def given_class_no_init(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "util_service.py").write_text(
        "class UtilService:\n    def do_work(self):\n        pass\n"
    )


# ---------------------------------------------------------------------------
# Givens - ADR-007
# ---------------------------------------------------------------------------


@given("a temporary source directory with no services subdirectory for ADR-007")
def given_no_services_dir_adr007(context):
    src = _make_tmp_src(context)
    app_dir = src / "application"
    app_dir.mkdir(parents=True)
    # no "services" subdirectory


@given("a temporary source directory with a service file importing from sqlalchemy")
def given_sqlalchemy_import(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "bad_svc.py").write_text("from sqlalchemy import Column\n")


@given("a temporary source directory with a service file using session.query")
def given_session_query(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "bad_svc.py").write_text(
        "def fetch(session):\n    return session.query(Model).all()\n"
    )


@given("a temporary source directory with a service file using session.execute")
def given_session_execute(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "bad_svc.py").write_text(
        "def run(session):\n    session.execute(stmt)\n"
    )


@given("a temporary source directory with a clean service file using no SQLAlchemy")
def given_clean_service(context):
    src = _make_tmp_src(context)
    svc_dir = src / "application" / "services"
    svc_dir.mkdir(parents=True)
    (svc_dir / "clean_svc.py").write_text(
        "class CleanService:\n    def run(self):\n        return 42\n"
    )


# ---------------------------------------------------------------------------
# Givens - main()
# ---------------------------------------------------------------------------


@given("a temporary project tree with clean source code for ADR compliance")
def given_clean_project_tree(context):
    context.tmp_root = tempfile.mkdtemp()
    src = Path(context.tmp_root) / "src" / "cleveragents"
    app_dir = src / "application"
    svc_dir = app_dir / "services"
    svc_dir.mkdir(parents=True)
    (app_dir / "clean.py").write_text("import asyncio\n")
    (svc_dir / "good.py").write_text(
        "class Good:\n    def __init__(self, dep):\n        self.dep = dep\n"
    )
    context.project_root = context.tmp_root


@given("a temporary project tree with ADR violations in the source code")
def given_violating_project_tree(context):
    context.tmp_root = tempfile.mkdtemp()
    src = Path(context.tmp_root) / "src" / "cleveragents"
    app_dir = src / "application"
    svc_dir = app_dir / "services"
    svc_dir.mkdir(parents=True)
    (app_dir / "bad.py").write_text("import threading\n")
    (svc_dir / "bad_svc.py").write_text("from sqlalchemy import Column\n")
    context.project_root = context.tmp_root


# ---------------------------------------------------------------------------
# Whens - ADR-002
# ---------------------------------------------------------------------------


@when("the ADR-002 async model checker runs against that source directory")
def when_run_adr002(context):
    context.adr002_result = _adr.check_adr002_async_model(context.src_dir)
    _cleanup(context)


# ---------------------------------------------------------------------------
# Whens - ADR-003
# ---------------------------------------------------------------------------


@when("the ADR-003 dependency injection checker runs against that source directory")
def when_run_adr003(context):
    context.adr003_result = _adr.check_adr003_dependency_injection(context.src_dir)
    _cleanup(context)


# ---------------------------------------------------------------------------
# Whens - ADR-007
# ---------------------------------------------------------------------------


@when("the ADR-007 repository pattern checker runs against that source directory")
def when_run_adr007(context):
    context.adr007_result = _adr.check_adr007_repository_pattern(context.src_dir)
    _cleanup(context)


# ---------------------------------------------------------------------------
# Whens - main()
# ---------------------------------------------------------------------------


@when("the ADR compliance main function runs with a nonexistent source directory")
def when_main_no_src(context):
    context.tmp_root = tempfile.mkdtemp()
    captured = StringIO()
    original_cwd = os.getcwd()
    try:
        os.chdir(context.tmp_root)
        with patch("sys.stdout", captured):
            context.main_exit_code = _adr.main()
    finally:
        os.chdir(original_cwd)
    context.main_output = captured.getvalue()
    _cleanup(context)


@when("the ADR compliance main function runs against that project tree")
def when_main_project_tree(context):
    captured = StringIO()
    original_cwd = os.getcwd()
    try:
        os.chdir(context.project_root)
        with patch("sys.stdout", captured):
            context.main_exit_code = _adr.main()
    finally:
        os.chdir(original_cwd)
    context.main_output = captured.getvalue()
    _cleanup(context)


# ---------------------------------------------------------------------------
# Thens - ADR-002
# ---------------------------------------------------------------------------


@then("the ADR-002 checker should return an empty violations list")
def then_adr002_empty(context):
    assert context.adr002_result == [], (
        f"Expected no ADR-002 violations but got: {context.adr002_result}"
    )


@then('the ADR-002 checker should return a violation mentioning "{fragment}"')
def then_adr002_violation_contains(context, fragment):
    assert len(context.adr002_result) > 0, "Expected at least one ADR-002 violation"
    matched = any(fragment in v for v in context.adr002_result)
    assert matched, (
        f"Expected a violation containing '{fragment}', got: {context.adr002_result}"
    )


# ---------------------------------------------------------------------------
# Thens - ADR-003
# ---------------------------------------------------------------------------


@then("the ADR-003 checker should return an empty violations list")
def then_adr003_empty(context):
    assert context.adr003_result == [], (
        f"Expected no ADR-003 violations but got: {context.adr003_result}"
    )


@then('the ADR-003 checker should return a violation mentioning "{fragment}"')
def then_adr003_violation_contains(context, fragment):
    assert len(context.adr003_result) > 0, "Expected at least one ADR-003 violation"
    matched = any(fragment in v for v in context.adr003_result)
    assert matched, (
        f"Expected a violation containing '{fragment}', got: {context.adr003_result}"
    )


# ---------------------------------------------------------------------------
# Thens - ADR-007
# ---------------------------------------------------------------------------


@then("the ADR-007 checker should return an empty violations list")
def then_adr007_empty(context):
    assert context.adr007_result == [], (
        f"Expected no ADR-007 violations but got: {context.adr007_result}"
    )


@then('the ADR-007 checker should return a violation mentioning "{fragment}"')
def then_adr007_violation_contains(context, fragment):
    assert len(context.adr007_result) > 0, "Expected at least one ADR-007 violation"
    matched = any(fragment in v for v in context.adr007_result)
    assert matched, (
        f"Expected a violation containing '{fragment}', got: {context.adr007_result}"
    )


# ---------------------------------------------------------------------------
# Thens - main()
# ---------------------------------------------------------------------------


@then("the ADR compliance main should return exit code {code:d}")
def then_main_exit_code(context, code):
    assert context.main_exit_code == code, (
        f"Expected exit code {code} but got {context.main_exit_code}"
    )


@then('the ADR compliance main output should contain "{fragment}"')
def then_main_output_contains(context, fragment):
    assert fragment in context.main_output, (
        f"Expected output containing '{fragment}', got:\n{context.main_output}"
    )
