"""Step definitions for devcontainer_content_ops_coverage.feature.

Exercises uncovered lines 223-348 in devcontainer.py:
- read()            (lines 223-238)
- write()           (lines 254-280)
- discover_children() (lines 294-328)
- _exec_ls()        (lines 333-348)

All subprocess.run calls are mocked via unittest.mock.patch.
Step prefix: dccov3 (devcontainer coverage round 3).
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.resource.handlers.devcontainer import DevcontainerHandler

# Valid 26-char Crockford Base32 ULID for test resources.
_TEST_ULID = "01JTESTDCC0V3RES0000000000"


def _make_resource(
    location: str | None = "/ws/project",
    resource_id: str = _TEST_ULID,
) -> Resource:
    """Create a minimal devcontainer-instance Resource for testing."""
    return Resource(
        resource_id=resource_id,
        name="test-devcontainer",
        resource_type_name="devcontainer-instance",
        classification=PhysVirt.PHYSICAL,
        description="Test devcontainer resource",
        location=location,
    )


# ── Given steps ──────────────────────────────────────────────


@given("dccov3 a devcontainer handler")
def step_dccov3_create_handler(context: Context) -> None:
    context.dccov3_handler = DevcontainerHandler()


@given('dccov3 a devcontainer-instance resource with location "{loc}"')
def step_dccov3_resource_with_location(context: Context, loc: str) -> None:
    context.dccov3_resource = _make_resource(location=loc)


@given("dccov3 a devcontainer-instance resource with no location")
def step_dccov3_resource_no_location(context: Context) -> None:
    context.dccov3_resource = _make_resource(location=None)


@given('dccov3 subprocess returns successful cat output "{text}"')
def step_dccov3_subprocess_cat_success(context: Context, text: str) -> None:
    mock_result = MagicMock()
    mock_result.returncode = 0
    mock_result.stdout = text.encode("utf-8")
    mock_result.stderr = b""
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "cat_success"


@given('dccov3 subprocess returns successful ls output "{text}"')
def step_dccov3_subprocess_ls_success(context: Context, text: str) -> None:
    mock_result = MagicMock()
    mock_result.returncode = 0
    # For ls via read (bytes), and for ls via discover_children (text mode)
    raw = text.replace("\\n", "\n")
    mock_result.stdout = raw.encode("utf-8")
    mock_result.stderr = b""
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "ls_success"


@given('dccov3 subprocess returns failed cat output with stderr "{stderr}"')
def step_dccov3_subprocess_cat_failure(context: Context, stderr: str) -> None:
    mock_result = MagicMock()
    mock_result.returncode = 1
    mock_result.stdout = b""
    mock_result.stderr = stderr.encode("utf-8")
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "cat_failure"


@given("dccov3 subprocess returns successful tee output")
def step_dccov3_subprocess_tee_success(context: Context) -> None:
    mock_result = MagicMock()
    mock_result.returncode = 0
    mock_result.stdout = b""
    mock_result.stderr = b""
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "tee_success"


@given('dccov3 subprocess returns failed tee output with stderr "{stderr}"')
def step_dccov3_subprocess_tee_failure(context: Context, stderr: str) -> None:
    mock_result = MagicMock()
    mock_result.returncode = 1
    mock_result.stdout = b""
    mock_result.stderr = stderr.encode("utf-8")
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "tee_failure"


@given('dccov3 subprocess returns successful ls listing "{text}"')
def step_dccov3_subprocess_ls_listing(context: Context, text: str) -> None:
    """Configure mock for discover_children which uses text=True."""
    mock_result = MagicMock()
    mock_result.returncode = 0
    raw = text.replace("\\n", "\n")
    mock_result.stdout = raw
    mock_result.stderr = ""
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "ls_listing"


@given("dccov3 subprocess returns failed ls output")
def step_dccov3_subprocess_ls_failure(context: Context) -> None:
    mock_result = MagicMock()
    mock_result.returncode = 1
    mock_result.stdout = ""
    mock_result.stderr = "ls: cannot access"
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "ls_failure"


@given("dccov3 subprocess returns failed ls for exec_ls")
def step_dccov3_subprocess_ls_failure_for_exec_ls(context: Context) -> None:
    """Configure mock for _exec_ls failure path (bytes mode, not text mode)."""
    mock_result = MagicMock()
    mock_result.returncode = 1
    mock_result.stdout = b""
    mock_result.stderr = b"ls: error"
    context.dccov3_mock_result = mock_result
    context.dccov3_mock_mode = "exec_ls_failure"


# ── When steps ───────────────────────────────────────────────


def _do_read(context: Context, path: str) -> None:
    """Shared implementation for read steps."""
    context.dccov3_error = None
    context.dccov3_read_result = None
    context.dccov3_subprocess_calls = []

    if not hasattr(context, "dccov3_mock_result"):
        # No subprocess mock configured — expect _require_location to fail
        try:
            context.dccov3_read_result = context.dccov3_handler.read(
                resource=context.dccov3_resource,
                path=path,
            )
        except (ValueError, FileNotFoundError, NotImplementedError) as exc:
            context.dccov3_error = exc
        return

    def _capture_run(*args, **kwargs):
        context.dccov3_subprocess_calls.append((args, kwargs))
        return context.dccov3_mock_result

    patcher = patch(
        "cleveragents.resource.handlers.devcontainer.subprocess.run",
        side_effect=_capture_run,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)

    try:
        context.dccov3_read_result = context.dccov3_handler.read(
            resource=context.dccov3_resource,
            path=path,
        )
    except (ValueError, FileNotFoundError, NotImplementedError) as exc:
        context.dccov3_error = exc


@when("dccov3 I read with an empty path from the resource")
def step_dccov3_read_empty_path(context: Context) -> None:
    """Dedicated step for reading with empty path (Behave can't match empty string)."""
    _do_read(context, path="")


@when('dccov3 I read path "{path}" from the resource')
def step_dccov3_read_path(context: Context, path: str) -> None:
    _do_read(context, path=path)


@when('dccov3 I write path "{path}" with data "{data}" to the resource')
def step_dccov3_write_path(context: Context, path: str, data: str) -> None:
    context.dccov3_error = None
    context.dccov3_write_result = None
    context.dccov3_subprocess_calls = []
    data_bytes = data.encode("utf-8")

    if not hasattr(context, "dccov3_mock_result"):
        try:
            context.dccov3_write_result = context.dccov3_handler.write(
                resource=context.dccov3_resource,
                path=path,
                data=data_bytes,
            )
        except (ValueError, NotImplementedError) as exc:
            context.dccov3_error = exc
        return

    def _capture_run(*args, **kwargs):
        context.dccov3_subprocess_calls.append((args, kwargs))
        return context.dccov3_mock_result

    patcher = patch(
        "cleveragents.resource.handlers.devcontainer.subprocess.run",
        side_effect=_capture_run,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)

    try:
        context.dccov3_write_result = context.dccov3_handler.write(
            resource=context.dccov3_resource,
            path=path,
            data=data_bytes,
        )
    except (ValueError, NotImplementedError) as exc:
        context.dccov3_error = exc


@when('dccov3 I write path "{path}" with bytes to the resource')
def step_dccov3_write_bytes(context: Context, path: str) -> None:
    """Write binary data to verify subprocess receives bytes as input."""
    context.dccov3_error = None
    context.dccov3_write_result = None
    context.dccov3_subprocess_calls = []
    context.dccov3_write_bytes = b"\x00\x01\x02\xff"

    def _capture_run(*args, **kwargs):
        context.dccov3_subprocess_calls.append((args, kwargs))
        return context.dccov3_mock_result

    patcher = patch(
        "cleveragents.resource.handlers.devcontainer.subprocess.run",
        side_effect=_capture_run,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)

    try:
        context.dccov3_write_result = context.dccov3_handler.write(
            resource=context.dccov3_resource,
            path=path,
            data=context.dccov3_write_bytes,
        )
    except (ValueError, NotImplementedError) as exc:
        context.dccov3_error = exc


@when("dccov3 I discover children of the resource")
def step_dccov3_discover_children(context: Context) -> None:
    context.dccov3_error = None
    context.dccov3_children = None
    context.dccov3_subprocess_calls = []

    if not hasattr(context, "dccov3_mock_result"):
        try:
            context.dccov3_children = context.dccov3_handler.discover_children(
                resource=context.dccov3_resource,
            )
        except (ValueError, NotImplementedError) as exc:
            context.dccov3_error = exc
        return

    def _capture_run(*args, **kwargs):
        context.dccov3_subprocess_calls.append((args, kwargs))
        return context.dccov3_mock_result

    patcher = patch(
        "cleveragents.resource.handlers.devcontainer.subprocess.run",
        side_effect=_capture_run,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)

    try:
        context.dccov3_children = context.dccov3_handler.discover_children(
            resource=context.dccov3_resource,
        )
    except (ValueError, NotImplementedError) as exc:
        context.dccov3_error = exc


# ── Then steps ───────────────────────────────────────────────


@then('dccov3 the read should succeed with data "{expected}"')
def step_dccov3_read_success(context: Context, expected: str) -> None:
    assert context.dccov3_error is None, (
        f"Expected no error, got {context.dccov3_error!r}"
    )
    assert context.dccov3_read_result is not None, "No read result returned"
    raw_expected = expected.replace("\\n", "\n")
    actual = context.dccov3_read_result.data.decode("utf-8")
    assert actual == raw_expected, f"Expected data {raw_expected!r}, got {actual!r}"


@then("dccov3 the read should succeed with empty data")
def step_dccov3_read_success_empty(context: Context) -> None:
    assert context.dccov3_error is None, (
        f"Expected no error, got {context.dccov3_error!r}"
    )
    assert context.dccov3_read_result is not None, "No read result returned"
    assert context.dccov3_read_result.data == b"", (
        f"Expected empty data, got {context.dccov3_read_result.data!r}"
    )


@then('dccov3 the read should raise FileNotFoundError containing "{text}"')
def step_dccov3_read_fnf_error(context: Context, text: str) -> None:
    assert context.dccov3_error is not None, "Expected FileNotFoundError"
    assert isinstance(context.dccov3_error, FileNotFoundError), (
        f"Expected FileNotFoundError, got {type(context.dccov3_error).__name__}"
    )
    assert text in str(context.dccov3_error), (
        f"Expected '{text}' in error message: {context.dccov3_error}"
    )


@then('dccov3 the read should raise ValueError containing "{text}"')
def step_dccov3_read_value_error(context: Context, text: str) -> None:
    assert context.dccov3_error is not None, "Expected ValueError"
    assert isinstance(context.dccov3_error, ValueError), (
        f"Expected ValueError, got {type(context.dccov3_error).__name__}"
    )
    assert text in str(context.dccov3_error), (
        f"Expected '{text}' in error message: {context.dccov3_error}"
    )


@then(
    'dccov3 the subprocess should have been called with command containing "{cmd}" and "{arg}"'
)
def step_dccov3_check_subprocess_args(context: Context, cmd: str, arg: str) -> None:
    assert len(context.dccov3_subprocess_calls) > 0, "No subprocess calls recorded"
    # args[0] is the positional arg (the command list)
    call_args, _call_kwargs = context.dccov3_subprocess_calls[0]
    cmd_list = call_args[0]  # First positional argument is the command list
    assert cmd in cmd_list, f"Expected '{cmd}' in command {cmd_list}"
    assert arg in cmd_list, f"Expected '{arg}' in command {cmd_list}"


@then("dccov3 the write should succeed with bytes_written {count:d}")
def step_dccov3_write_success(context: Context, count: int) -> None:
    assert context.dccov3_error is None, (
        f"Expected no error, got {context.dccov3_error!r}"
    )
    assert context.dccov3_write_result is not None, "No write result returned"
    assert context.dccov3_write_result.success is True, (
        f"Expected success, got failure: {context.dccov3_write_result.message}"
    )
    assert context.dccov3_write_result.bytes_written == count, (
        f"Expected {count} bytes written, got {context.dccov3_write_result.bytes_written}"
    )


@then('dccov3 the write message should contain "{text}"')
def step_dccov3_write_msg_contains(context: Context, text: str) -> None:
    assert context.dccov3_write_result is not None
    assert text in context.dccov3_write_result.message, (
        f"Expected '{text}' in message: {context.dccov3_write_result.message}"
    )


@then("dccov3 the write should fail")
def step_dccov3_write_failure(context: Context) -> None:
    assert context.dccov3_error is None, (
        f"Unexpected exception: {context.dccov3_error!r}"
    )
    assert context.dccov3_write_result is not None, "No write result returned"
    assert context.dccov3_write_result.success is False, (
        "Expected write to fail but it succeeded"
    )


@then('dccov3 the write failure message should contain "{text}"')
def step_dccov3_write_fail_msg(context: Context, text: str) -> None:
    assert context.dccov3_write_result is not None
    assert context.dccov3_write_result.success is False
    assert text in context.dccov3_write_result.message, (
        f"Expected '{text}' in failure message: {context.dccov3_write_result.message}"
    )


@then('dccov3 the write should raise ValueError containing "{text}"')
def step_dccov3_write_value_error(context: Context, text: str) -> None:
    assert context.dccov3_error is not None, "Expected ValueError"
    assert isinstance(context.dccov3_error, ValueError), (
        f"Expected ValueError, got {type(context.dccov3_error).__name__}"
    )
    assert text in str(context.dccov3_error), (
        f"Expected '{text}' in error message: {context.dccov3_error}"
    )


@then("dccov3 the children list should have {count:d} entries")
def step_dccov3_children_count(context: Context, count: int) -> None:
    assert context.dccov3_error is None, (
        f"Expected no error, got {context.dccov3_error!r}"
    )
    assert context.dccov3_children is not None, "No children result"
    assert len(context.dccov3_children) == count, (
        f"Expected {count} children, got {len(context.dccov3_children)}"
    )


@then('dccov3 the first child should have name "{name}"')
def step_dccov3_first_child_name(context: Context, name: str) -> None:
    assert len(context.dccov3_children) >= 1, "No children found"
    assert context.dccov3_children[0].name == name, (
        f"Expected first child name '{name}', got '{context.dccov3_children[0].name}'"
    )


@then('dccov3 the second child should have name "{name}"')
def step_dccov3_second_child_name(context: Context, name: str) -> None:
    assert len(context.dccov3_children) >= 2, "Fewer than 2 children found"
    assert context.dccov3_children[1].name == name, (
        f"Expected second child name '{name}', got '{context.dccov3_children[1].name}'"
    )


@then('dccov3 the third child should have name "{name}"')
def step_dccov3_third_child_name(context: Context, name: str) -> None:
    assert len(context.dccov3_children) >= 3, "Fewer than 3 children found"
    assert context.dccov3_children[2].name == name, (
        f"Expected third child name '{name}', got '{context.dccov3_children[2].name}'"
    )


@then('dccov3 each child should have type "{type_name}"')
def step_dccov3_children_type(context: Context, type_name: str) -> None:
    for child in context.dccov3_children:
        assert child.resource_type_name == type_name, (
            f"Expected type '{type_name}', got '{child.resource_type_name}'"
        )


@then('dccov3 each child location should start with "{prefix}"')
def step_dccov3_children_location_prefix(context: Context, prefix: str) -> None:
    for child in context.dccov3_children:
        assert child.location is not None, f"Child '{child.name}' has no location"
        assert child.location.startswith(prefix), (
            f"Expected location to start with '{prefix}', got '{child.location}'"
        )


@then("dccov3 each child should reference the parent resource id")
def step_dccov3_children_parent_ref(context: Context) -> None:
    parent_id = context.dccov3_resource.resource_id
    for child in context.dccov3_children:
        assert parent_id in child.parents, (
            f"Expected parent {parent_id} in child.parents {child.parents}"
        )


@then('dccov3 discover children should raise ValueError containing "{text}"')
def step_dccov3_discover_value_error(context: Context, text: str) -> None:
    assert context.dccov3_error is not None, "Expected ValueError"
    assert isinstance(context.dccov3_error, ValueError), (
        f"Expected ValueError, got {type(context.dccov3_error).__name__}"
    )
    assert text in str(context.dccov3_error), (
        f"Expected '{text}' in error message: {context.dccov3_error}"
    )


@then("dccov3 the subprocess should have received input bytes")
def step_dccov3_subprocess_input_bytes(context: Context) -> None:
    assert len(context.dccov3_subprocess_calls) > 0, "No subprocess calls recorded"
    _call_args, call_kwargs = context.dccov3_subprocess_calls[0]
    assert "input" in call_kwargs, "subprocess.run was not called with 'input' kwarg"
    assert call_kwargs["input"] == context.dccov3_write_bytes, (
        f"Expected input {context.dccov3_write_bytes!r}, got {call_kwargs['input']!r}"
    )


@then("dccov3 each child description should mention the parent resource id")
def step_dccov3_children_description_parent(context: Context) -> None:
    parent_id = context.dccov3_resource.resource_id
    for child in context.dccov3_children:
        assert child.description is not None, f"Child '{child.name}' has no description"
        assert parent_id in child.description, (
            f"Expected parent id '{parent_id}' in description '{child.description}'"
        )
