"""Step definitions for project_cli_coverage_r2.feature.

Exercises the remaining uncovered lines in
``cleveragents.cli.commands.project``:

* Lines 73, 75, 76 - ``_get_namespaced_project_repo()`` body
* Lines 81, 83, 84 - ``_get_resource_link_repo()`` body
* Lines 89, 91, 92 - ``_get_resource_registry_service()`` body
* Line 152          - ``str(lr.linked_at)`` fallback (no ``.isoformat``)
* Line 164          - ``str(project.created_at)`` fallback
* Line 167          - ``str(project.updated_at)`` fallback
* Line 843          - ``str(proj.created_at)[:10]`` fallback in list table
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from typer.testing import CliRunner

from cleveragents.cli.commands.project import (
    _project_spec_dict,
    app,
)

runner = CliRunner()

# Patch targets
_PATCH_PROJECT_REPO = "cleveragents.cli.commands.project._get_namespaced_project_repo"
_PATCH_CONTAINER = "cleveragents.cli.commands.project.get_container"


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


def _make_linked_resource(
    resource_id: str = "res-100",
    project_read_only: bool = False,
    alias: str | None = None,
    linked_at: Any = None,
) -> MagicMock:
    """Build a mock linked-resource object."""
    lr = MagicMock()
    lr.resource_id = resource_id
    lr.project_read_only = project_read_only
    lr.alias = alias
    lr.linked_at = (
        linked_at if linked_at is not None else datetime(2025, 3, 1, tzinfo=UTC)
    )
    return lr


def _make_project(
    namespaced_name: str = "local/r2-proj",
    namespace: str = "local",
    name: str = "r2-proj",
    description: str | None = "Round-2 project",
    linked_resources: list | None = None,
    created_at: Any = None,
    updated_at: Any = None,
) -> MagicMock:
    """Build a mock NamespacedProject."""
    proj = MagicMock()
    proj.namespaced_name = namespaced_name
    proj.namespace = namespace
    proj.name = name
    proj.description = description
    proj.linked_resources = linked_resources if linked_resources is not None else []
    proj.created_at = (
        created_at if created_at is not None else datetime(2025, 1, 1, tzinfo=UTC)
    )
    proj.updated_at = (
        updated_at if updated_at is not None else datetime(2025, 1, 2, tzinfo=UTC)
    )
    return proj


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("the r2 project CLI mocks are prepared")
def step_r2_prepare(context: Any) -> None:
    context.r2_result = None
    context.r2_helper_result = None
    context.r2_spec_dict = None
    context.r2_project = None


# ---------------------------------------------------------------------------
# _get_namespaced_project_repo  (lines 73, 75, 76)
# ---------------------------------------------------------------------------


@when("I call the actual _get_namespaced_project_repo helper with a mocked container")
def step_call_get_namespaced_project_repo(context: Any) -> None:
    mock_container = MagicMock()
    mock_repo = MagicMock(spec_name="MockNamespacedProjectRepo")
    mock_container.namespaced_project_repo.return_value = mock_repo

    # Patch get_container where it is imported inside the function body.
    # The function does: ``from cleveragents.application.container import get_container``
    # so we patch the canonical location.
    with patch(
        "cleveragents.application.container.get_container",
        return_value=mock_container,
    ):
        from cleveragents.cli.commands.project import _get_namespaced_project_repo

        context.r2_helper_result = _get_namespaced_project_repo()
        context.r2_expected_repo = mock_repo


@then("the r2 helper result should be the mock namespaced project repo")
def step_assert_namespaced_project_repo(context: Any) -> None:
    assert context.r2_helper_result is context.r2_expected_repo, (
        f"Expected the mock repo, got {context.r2_helper_result!r}"
    )


# ---------------------------------------------------------------------------
# _get_resource_link_repo  (lines 81, 83, 84)
# ---------------------------------------------------------------------------


@when("I call the actual _get_resource_link_repo helper with a mocked container")
def step_call_get_resource_link_repo(context: Any) -> None:
    mock_container = MagicMock()
    mock_link_repo = MagicMock(spec_name="MockResourceLinkRepo")
    mock_container.project_resource_link_repo.return_value = mock_link_repo

    with patch(
        "cleveragents.application.container.get_container",
        return_value=mock_container,
    ):
        from cleveragents.cli.commands.project import _get_resource_link_repo

        context.r2_helper_result = _get_resource_link_repo()
        context.r2_expected_repo = mock_link_repo


@then("the r2 helper result should be the mock resource link repo")
def step_assert_resource_link_repo(context: Any) -> None:
    assert context.r2_helper_result is context.r2_expected_repo, (
        f"Expected the mock link repo, got {context.r2_helper_result!r}"
    )


# ---------------------------------------------------------------------------
# _get_resource_registry_service  (lines 89, 91, 92)
# ---------------------------------------------------------------------------


@when("I call the actual _get_resource_registry_service helper with a mocked container")
def step_call_get_resource_registry_service(context: Any) -> None:
    mock_container = MagicMock()
    mock_registry = MagicMock(spec_name="MockResourceRegistryService")
    mock_container.resource_registry_service.return_value = mock_registry

    with patch(
        "cleveragents.application.container.get_container",
        return_value=mock_container,
    ):
        from cleveragents.cli.commands.project import _get_resource_registry_service

        context.r2_helper_result = _get_resource_registry_service()
        context.r2_expected_repo = mock_registry


@then("the r2 helper result should be the mock resource registry service")
def step_assert_resource_registry_service(context: Any) -> None:
    assert context.r2_helper_result is context.r2_expected_repo, (
        f"Expected the mock registry, got {context.r2_helper_result!r}"
    )


# ---------------------------------------------------------------------------
# _project_spec_dict fallback: linked_at as string (line 152)
# ---------------------------------------------------------------------------


@given("a mock project with a linked resource whose linked_at is a plain string")
def step_project_with_string_linked_at(context: Any) -> None:
    lr = _make_linked_resource(linked_at="2025-03-01-string")
    # Remove the isoformat attribute so hasattr(lr.linked_at, "isoformat") is False
    # A plain string does NOT have isoformat, so just using a str is enough.
    context.r2_project = _make_project(linked_resources=[lr])


@when("I call _project_spec_dict on that project")
def step_call_project_spec_dict(context: Any) -> None:
    context.r2_spec_dict = _project_spec_dict(context.r2_project)


@then("the linked resource linked_at should be the plain string value")
def step_assert_linked_at_string(context: Any) -> None:
    lr_data = context.r2_spec_dict["linked_resources"][0]
    assert lr_data["linked_at"] == "2025-03-01-string", (
        f"Expected '2025-03-01-string', got {lr_data['linked_at']!r}"
    )


# ---------------------------------------------------------------------------
# _project_spec_dict fallback: created_at as string (line 164)
# ---------------------------------------------------------------------------


@given("a mock project whose created_at is a plain string")
def step_project_with_string_created_at(context: Any) -> None:
    context.r2_project = _make_project(
        created_at="2025-01-01-string",
        # Keep updated_at as a proper datetime so only line 164 fires
        updated_at=datetime(2025, 1, 2, tzinfo=UTC),
    )


@then("the result created_at should be the plain string value")
def step_assert_created_at_string(context: Any) -> None:
    assert context.r2_spec_dict["created_at"] == "2025-01-01-string", (
        f"Expected '2025-01-01-string', got {context.r2_spec_dict['created_at']!r}"
    )


# ---------------------------------------------------------------------------
# _project_spec_dict fallback: updated_at as string (line 167)
# ---------------------------------------------------------------------------


@given("a mock project whose updated_at is a plain string")
def step_project_with_string_updated_at(context: Any) -> None:
    context.r2_project = _make_project(
        # Keep created_at as a proper datetime so only line 167 fires
        created_at=datetime(2025, 1, 1, tzinfo=UTC),
        updated_at="2025-01-02-string",
    )


@then("the result updated_at should be the plain string value")
def step_assert_updated_at_string(context: Any) -> None:
    assert context.r2_spec_dict["updated_at"] == "2025-01-02-string", (
        f"Expected '2025-01-02-string', got {context.r2_spec_dict['updated_at']!r}"
    )


# ---------------------------------------------------------------------------
# _project_spec_dict: all dates are plain strings at once
# ---------------------------------------------------------------------------


@given("a mock project where all dates and linked_at are plain strings")
def step_project_with_all_string_dates(context: Any) -> None:
    lr = _make_linked_resource(linked_at="2025-03-01-lr-str")
    context.r2_project = _make_project(
        linked_resources=[lr],
        created_at="2025-01-01-cr-str",
        updated_at="2025-01-02-up-str",
    )


@then("all date fields in the result should be plain string values")
def step_assert_all_dates_string(context: Any) -> None:
    d = context.r2_spec_dict
    assert d["created_at"] == "2025-01-01-cr-str", (
        f"created_at mismatch: {d['created_at']!r}"
    )
    assert d["updated_at"] == "2025-01-02-up-str", (
        f"updated_at mismatch: {d['updated_at']!r}"
    )
    lr_data = d["linked_resources"][0]
    assert lr_data["linked_at"] == "2025-03-01-lr-str", (
        f"linked_at mismatch: {lr_data['linked_at']!r}"
    )


# ---------------------------------------------------------------------------
# list_projects rich table: created_at without strftime (line 843)
# ---------------------------------------------------------------------------


@given("the r2 project repo returns projects with string created_at values")
def step_project_repo_returns_string_dates(context: Any) -> None:
    # A project whose created_at is a plain string (no .strftime method)
    proj = _make_project(
        namespaced_name="local/str-date-proj",
        namespace="local",
        name="str-date-proj",
        description="Has string dates",
        created_at="2025-06-15T12:00:00",
        updated_at="2025-06-16T12:00:00",
    )
    context.r2_mock_repo = MagicMock()
    context.r2_mock_repo.list_projects.return_value = [proj]


@when("I invoke the r2 CLI list command with rich format")
def step_invoke_list_rich_format(context: Any) -> None:
    with patch(_PATCH_PROJECT_REPO, return_value=context.r2_mock_repo):
        context.r2_result = runner.invoke(app, ["list", "--format", "rich"])


@then("the r2 CLI output should contain the truncated string date")
def step_assert_list_contains_truncated_date(context: Any) -> None:
    output = context.r2_result.output
    # str(proj.created_at)[:10] should yield "2025-06-15"
    assert "2025-06-15" in output, f"Expected '2025-06-15' in output, got:\n{output}"


@then("the r2 CLI exit code should be {code:d}")
def step_r2_exit_code(context: Any, code: int) -> None:
    actual = context.r2_result.exit_code
    assert actual == code, (
        f"Expected exit code {code}, got {actual}. Output:\n{context.r2_result.output}"
    )
