"""Step definitions for git_checkout_handler_coverage_r3.feature.

All steps use the ``gcov`` prefix to avoid Behave AmbiguousStep errors.
Exercises uncovered lines in git_checkout.py (GitCheckoutHandler).
"""

from __future__ import annotations

import os
import subprocess
import tempfile
from unittest.mock import MagicMock, patch

from behave import given, then, when

from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler

_SUBPROCESS_RUN = "subprocess.run"
_HANDLER_MODULE = "cleveragents.resource.handlers.git_checkout"


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


def _make_resource(location: str | None = None) -> Resource:
    """Create a minimal Resource with a git-checkout type."""
    return Resource(
        resource_id="01JQDVHN5X5QJKBMZ3AP8Y4G7K",
        name="test-repo",
        resource_type_name="git-checkout",
        classification=PhysVirt.PHYSICAL,
        description="Test git checkout resource",
        location=location,
    )


def _init_git_repo(files: dict[str, bytes] | None = None) -> str:
    """Create a temporary git repo with an initial commit and optional files."""
    repo = tempfile.mkdtemp(prefix="gcov-repo-")
    subprocess.run(["git", "init"], cwd=repo, capture_output=True, check=True)
    subprocess.run(
        ["git", "config", "user.email", "test@gcov.dev"],
        cwd=repo,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "config", "user.name", "GCov Test"],
        cwd=repo,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "config", "commit.gpgSign", "false"],
        cwd=repo,
        capture_output=True,
        check=True,
    )
    # Always create a README so there's at least one tracked file
    readme = os.path.join(repo, "README.md")
    with open(readme, "w") as f:
        f.write("# gcov test\n")

    if files:
        for name, data in files.items():
            fpath = os.path.join(repo, name)
            os.makedirs(os.path.dirname(fpath), exist_ok=True)
            with open(fpath, "wb") as f:
                f.write(data)

    subprocess.run(["git", "add", "."], cwd=repo, capture_output=True, check=True)
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=repo,
        capture_output=True,
        check=True,
    )
    return repo


# ---------------------------------------------------------------------------
# GIVEN steps
# ---------------------------------------------------------------------------


@given("gcov a handler with a git repo containing tracked files")
def step_gcov_handler_with_tracked(ctx):
    repo = _init_git_repo({"src/app.py": b"print('hi')\n"})
    ctx.gcov_handler = GitCheckoutHandler()
    ctx.gcov_resource = _make_resource(location=repo)
    ctx.gcov_repo = repo
    ctx.gcov_error = None
    ctx.gcov_result = None


@given("gcov a handler with a git repo location")
def step_gcov_handler_plain(ctx):
    repo = _init_git_repo()
    ctx.gcov_handler = GitCheckoutHandler()
    ctx.gcov_resource = _make_resource(location=repo)
    ctx.gcov_repo = repo
    ctx.gcov_error = None
    ctx.gcov_result = None


@given('gcov a handler with a git repo and a real file "{filename}"')
def step_gcov_handler_with_file(ctx, filename):
    content = b"hello world\n"
    repo = _init_git_repo({filename: content})
    ctx.gcov_handler = GitCheckoutHandler()
    ctx.gcov_resource = _make_resource(location=repo)
    ctx.gcov_repo = repo
    ctx.gcov_file_content = content
    ctx.gcov_error = None
    ctx.gcov_result = None


@given('gcov a handler with a git repo and a real binary file "{filename}"')
def step_gcov_handler_with_binary_file(ctx, filename):
    # Non-UTF-8 binary content
    content = bytes(range(256))
    repo = _init_git_repo({filename: content})
    ctx.gcov_handler = GitCheckoutHandler()
    ctx.gcov_resource = _make_resource(location=repo)
    ctx.gcov_repo = repo
    ctx.gcov_file_content = content
    ctx.gcov_error = None
    ctx.gcov_result = None


@given('gcov a handler with a git repo containing subdirectory "{dirname}"')
def step_gcov_handler_with_subdir(ctx, dirname):
    files = {f"{dirname}/file.txt": b"data\n"}
    repo = _init_git_repo(files)
    ctx.gcov_handler = GitCheckoutHandler()
    ctx.gcov_resource = _make_resource(location=repo)
    ctx.gcov_repo = repo
    ctx.gcov_error = None
    ctx.gcov_result = None


@given('gcov git show returns binary data for path "{path}"')
def step_gcov_mock_git_show_binary(ctx, path):
    """Mock subprocess.run so that git show returns binary (non-UTF-8) data."""
    real_run = subprocess.run
    binary_data = bytes(range(128, 256))  # non-UTF-8

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "show" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 0
            mock_result.stdout = binary_data
            mock_result.stderr = b""
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)
    # We also need the file to exist for _safe_resolve
    fpath = os.path.join(ctx.gcov_repo, path)
    os.makedirs(os.path.dirname(fpath), exist_ok=True)
    with open(fpath, "wb") as f:
        f.write(binary_data)


@given("gcov subprocess run is mocked to raise TimeoutExpired for git show")
def step_gcov_mock_timeout_git_show(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "show" in cmd:
            raise subprocess.TimeoutExpired(cmd="git show", timeout=30)
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked to raise OSError for git show")
def step_gcov_mock_oserror_git_show(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "show" in cmd:
            raise OSError("mocked OS error")
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked to fail git show with nonzero rc")
def step_gcov_mock_git_show_fail(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "show" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 128
            mock_result.stdout = b""
            mock_result.stderr = b"fatal: not a git repo"
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked so git ls-tree returns nonzero")
def step_gcov_mock_lstree_fail(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "ls-tree" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 128
            mock_result.stdout = ""
            mock_result.stderr = "fatal: not a tree"
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked for diff with no changes")
def step_gcov_mock_diff_no_changes(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "diff" in cmd and "--no-index" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 0  # 0 = no diff
            mock_result.stdout = (
                b"" if isinstance(cmd, list) and "--shortstat" not in cmd else ""
            )
            mock_result.stderr = b""
            # Handle both text and binary mode
            if kwargs.get("text"):
                mock_result.stdout = ""
                mock_result.stderr = ""
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked so git ls-tree -d returns nonzero")
def step_gcov_mock_lstree_d_fail(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "ls-tree" in cmd and "-d" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 128
            mock_result.stdout = ""
            mock_result.stderr = "fatal: bad tree"
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked so git ls-tree -d returns blank lines")
def step_gcov_mock_lstree_d_blank_lines(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "ls-tree" in cmd and "-d" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 0
            mock_result.stdout = "\nrealdir\n\n \n"
            mock_result.stderr = ""
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov a handler and a resource with location")
def step_gcov_handler_and_resource(ctx):
    repo = _init_git_repo()
    ctx.gcov_handler = GitCheckoutHandler()
    ctx.gcov_resource = _make_resource(location=repo)
    ctx.gcov_repo = repo
    ctx.gcov_error = None
    ctx.gcov_result = None


@given("gcov a sandbox manager that returns None for get_sandbox")
def step_gcov_sandbox_mgr_none(ctx):
    mgr = MagicMock()
    mgr.get_sandbox.return_value = None
    ctx.gcov_sandbox_manager = mgr


@given("gcov a sandbox manager that returns a sandbox with context")
def step_gcov_sandbox_mgr_with_context(ctx):
    sandbox_ctx = MagicMock()
    sandbox_ctx.sandbox_path = ctx.gcov_repo

    sandbox = MagicMock()
    sandbox.context = sandbox_ctx

    mgr = MagicMock()
    mgr.get_sandbox.return_value = sandbox
    ctx.gcov_sandbox_manager = mgr


@given("gcov subprocess run is mocked so git tag returns nonzero")
def step_gcov_mock_git_tag_fail(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "tag" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 1
            mock_result.stdout = ""
            mock_result.stderr = "fatal: tag already exists"
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


@given("gcov subprocess run is mocked so git reset returns nonzero")
def step_gcov_mock_git_reset_fail(ctx):
    real_run = subprocess.run

    def side_effect(cmd, *args, **kwargs):
        if isinstance(cmd, list) and "reset" in cmd:
            mock_result = MagicMock()
            mock_result.returncode = 1
            mock_result.stdout = ""
            mock_result.stderr = "fatal: ambiguous argument"
            return mock_result
        return real_run(cmd, *args, **kwargs)

    patcher = patch(_SUBPROCESS_RUN, side_effect=side_effect)
    patcher.start()
    ctx.add_cleanup(patcher.stop)


# ---------------------------------------------------------------------------
# WHEN steps
# ---------------------------------------------------------------------------


@when("gcov read is called with empty path")
def step_gcov_read_empty_path(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.read(resource=ctx.gcov_resource, path="")
    except Exception as exc:
        ctx.gcov_error = exc


@when('gcov read is called with path "{path}"')
def step_gcov_read_path(ctx, path):
    try:
        ctx.gcov_result = ctx.gcov_handler.read(resource=ctx.gcov_resource, path=path)
    except Exception as exc:
        ctx.gcov_error = exc


@when('gcov read is called with path "{path}" expecting error')
def step_gcov_read_path_error(ctx, path):
    try:
        ctx.gcov_result = ctx.gcov_handler.read(resource=ctx.gcov_resource, path=path)
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov delete is called with empty path expecting error")
def step_gcov_delete_empty_path(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.delete(resource=ctx.gcov_resource, path="")
    except Exception as exc:
        ctx.gcov_error = exc


@when('gcov delete is called with path "{path}" expecting error')
def step_gcov_delete_path_error(ctx, path):
    try:
        ctx.gcov_result = ctx.gcov_handler.delete(resource=ctx.gcov_resource, path=path)
    except Exception as exc:
        ctx.gcov_error = exc


@when('gcov delete is called with path "{path}"')
def step_gcov_delete_path(ctx, path):
    try:
        ctx.gcov_result = ctx.gcov_handler.delete(resource=ctx.gcov_resource, path=path)
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov list_children is called")
def step_gcov_list_children(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.list_children(resource=ctx.gcov_resource)
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov diff is called with the same location as other")
def step_gcov_diff_same(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.diff(
            resource=ctx.gcov_resource,
            other_location=ctx.gcov_resource.location,
        )
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov discover_children is called")
def step_gcov_discover_children(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.discover_children(resource=ctx.gcov_resource)
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov create_checkpoint is called expecting error")
def step_gcov_create_checkpoint_error(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.create_checkpoint(
            resource=ctx.gcov_resource,
            plan_id="plan-gcov-001",
            sandbox_manager=ctx.gcov_sandbox_manager,
            phase="execution",
        )
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov rollback_to is called expecting error")
def step_gcov_rollback_error(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.rollback_to(
            resource=ctx.gcov_resource,
            plan_id="plan-gcov-001",
            checkpoint_id="checkpoint-gcov-20260101T000000",
            sandbox_manager=ctx.gcov_sandbox_manager,
        )
    except Exception as exc:
        ctx.gcov_error = exc


@when("gcov rollback_to is called")
def step_gcov_rollback(ctx):
    try:
        ctx.gcov_result = ctx.gcov_handler.rollback_to(
            resource=ctx.gcov_resource,
            plan_id="plan-gcov-001",
            checkpoint_id="checkpoint-gcov-20260101T000000",
            sandbox_manager=ctx.gcov_sandbox_manager,
        )
    except Exception as exc:
        ctx.gcov_error = exc


# ---------------------------------------------------------------------------
# THEN steps
# ---------------------------------------------------------------------------


@then("gcov the result should be a Content with newline-joined file listing")
def step_gcov_check_content_listing(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a Content result"
    text = ctx.gcov_result.data.decode("utf-8")
    assert ctx.gcov_result.encoding == "utf-8"
    # The repo has README.md and src/app.py tracked; listing should have entries
    assert len(text.strip()) > 0, "Expected non-empty file listing"


@then("gcov the result encoding should be None")
def step_gcov_encoding_none(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a Content result"
    assert ctx.gcov_result.encoding is None, (
        f"Expected encoding=None, got {ctx.gcov_result.encoding!r}"
    )


@then('gcov the result data should match the file contents of "{filename}"')
def step_gcov_data_matches_file(ctx, filename):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a Content result"
    assert ctx.gcov_result.data == ctx.gcov_file_content, (
        f"Data mismatch: expected {ctx.gcov_file_content!r}, got {ctx.gcov_result.data!r}"
    )


@then("gcov a FileNotFoundError should be stored")
def step_gcov_fnf_error(ctx):
    assert ctx.gcov_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gcov_error, FileNotFoundError), (
        f"Expected FileNotFoundError, got {type(ctx.gcov_error).__name__}: {ctx.gcov_error}"
    )


@then('gcov the result encoding should be "{encoding}"')
def step_gcov_encoding_value(ctx, encoding):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a result"
    assert ctx.gcov_result.encoding == encoding, (
        f"Expected encoding={encoding!r}, got {ctx.gcov_result.encoding!r}"
    )


@then("gcov the result should have a content_hash")
def step_gcov_has_hash(ctx):
    assert ctx.gcov_result is not None, "Expected a result"
    assert ctx.gcov_result.content_hash is not None, "Expected content_hash to be set"
    assert len(ctx.gcov_result.content_hash) == 64, (
        f"Expected SHA-256 hex digest (64 chars), got {len(ctx.gcov_result.content_hash)}"
    )


@then("gcov a PermissionError should be stored")
def step_gcov_perm_error(ctx):
    assert ctx.gcov_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gcov_error, PermissionError), (
        f"Expected PermissionError, got {type(ctx.gcov_error).__name__}: {ctx.gcov_error}"
    )


@then("gcov the delete result should indicate success")
def step_gcov_delete_success(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a DeleteResult"
    assert ctx.gcov_result.success is True, "Expected success=True"


@then('gcov the directory "{dirname}" should no longer exist')
def step_gcov_dir_gone(ctx, dirname):
    target = os.path.join(ctx.gcov_repo, dirname)
    assert not os.path.exists(target), f"Directory {target} still exists"


@then("gcov the result should be a sorted list of filesystem entries")
def step_gcov_sorted_fs_list(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert isinstance(ctx.gcov_result, list), (
        f"Expected list, got {type(ctx.gcov_result).__name__}"
    )
    # The fallback os.listdir should return at least README.md
    assert len(ctx.gcov_result) > 0, "Expected non-empty listing"
    assert ctx.gcov_result == sorted(ctx.gcov_result), "Expected sorted order"


@then("gcov the diff result has_changes should be False")
def step_gcov_diff_no_changes(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a DiffResult"
    assert ctx.gcov_result.has_changes is False, (
        f"Expected has_changes=False, got {ctx.gcov_result.has_changes}"
    )


@then("gcov the diff result unified_diff should be empty")
def step_gcov_diff_empty(ctx):
    assert ctx.gcov_result.unified_diff == "", (
        f"Expected empty unified_diff, got {ctx.gcov_result.unified_diff!r}"
    )


@then("gcov the result should be an empty list")
def step_gcov_empty_list(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result == [], f"Expected empty list, got {ctx.gcov_result!r}"


@then("gcov the result should contain only non-empty directory resources")
def step_gcov_non_empty_dir_resources(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert isinstance(ctx.gcov_result, list), (
        f"Expected list, got {type(ctx.gcov_result).__name__}"
    )
    # "realdir" should be present; empty / whitespace-only names should be skipped
    names = [r.name for r in ctx.gcov_result]
    assert "" not in names, "Empty dirname should have been skipped"
    assert "realdir" in names, f"Expected 'realdir' in result names, got {names}"


@then('gcov a RuntimeError should be stored with message "{msg}"')
def step_gcov_runtime_error_msg(ctx, msg):
    assert ctx.gcov_error is not None, "Expected an error but none occurred"
    assert isinstance(ctx.gcov_error, RuntimeError), (
        f"Expected RuntimeError, got {type(ctx.gcov_error).__name__}: {ctx.gcov_error}"
    )
    assert msg in str(ctx.gcov_error), (
        f"Expected '{msg}' in error message, got: {ctx.gcov_error}"
    )


@then("gcov the rollback result success should be False")
def step_gcov_rollback_fail(ctx):
    assert ctx.gcov_error is None, f"Unexpected error: {ctx.gcov_error}"
    assert ctx.gcov_result is not None, "Expected a RollbackResult"
    assert ctx.gcov_result.success is False, (
        f"Expected success=False, got {ctx.gcov_result.success}"
    )


@then('gcov the rollback result message should contain "{msg}"')
def step_gcov_rollback_msg(ctx, msg):
    assert msg in ctx.gcov_result.message, (
        f"Expected '{msg}' in message, got: {ctx.gcov_result.message!r}"
    )
