"""Step definitions for multi_project_service_coverage_boost.feature.

Targets uncovered lines in multi_project_service.py:
  - Line 59:  constructor ValueError when decision_service is None
  - Line 66:  decision_service property accessor
  - Line 93:  initialize_scopes ValueError when plan is None
  - Line 95:  initialize_scopes ValueError when available_resources is None
  - Line 157: resolve_context_view ValueError when plan is None
  - Line 196: record_changeset ValueError when plan is None
  - Lines 198-199: record_changeset ValueError when metadata not initialized
  - Line 253: validate_cross_project returns error for None plan
  - Line 255: validate_cross_project returns [] for plan without metadata
  - Lines 283-285: validate_cross_project detects unknown source project
"""

from __future__ import annotations

from unittest.mock import MagicMock

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

from cleveragents.application.services.multi_project_service import (
    MultiProjectService,
)
from cleveragents.domain.models.core.multi_project import (
    ChangeSetSummary,
    CrossProjectDependency,
)
from cleveragents.domain.models.core.plan import (
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    ProcessingState,
    ProjectLink,
)

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

_PLAN_ID = "01HXBBBBBBBBBBBBBBBBBBBBBB"


def _make_plan(project_links: list[ProjectLink]) -> Plan:
    """Build a minimal Plan with the given project links."""
    return Plan(
        identity=PlanIdentity(plan_id=_PLAN_ID),
        namespaced_name=NamespacedName.parse("local/cov-boost-test"),
        description="Coverage boost test plan",
        action_name="local/test-action",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
        project_links=project_links,
    )


def _mock_decision_service() -> MagicMock:
    mock = MagicMock()
    mock.list_by_type.return_value = []
    return mock


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


@given("a fresh multi-project service")
def step_fresh_service(context: Context) -> None:
    context.decision_service_mock = _mock_decision_service()
    context.service = MultiProjectService(
        decision_service=context.decision_service_mock,
    )
    context.caught_exception = None
    context.validation_result = None
    context.plan = None


# ---------------------------------------------------------------------------
# Constructor guard - None decision_service (line 59)
# ---------------------------------------------------------------------------


@when("I create a MultiProjectService with a None decision_service")
def step_create_service_none(context: Context) -> None:
    try:
        MultiProjectService(decision_service=None)  # type: ignore[arg-type]
    except ValueError as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# decision_service property (line 66)
# ---------------------------------------------------------------------------


@then("the decision_service property should return the injected mock")
def step_check_property(context: Context) -> None:
    assert context.service.decision_service is context.decision_service_mock, (
        "decision_service property did not return the injected mock"
    )


# ---------------------------------------------------------------------------
# initialize_scopes - None plan (line 93)
# ---------------------------------------------------------------------------


@when("I call initialize_scopes with a None plan")
def step_init_scopes_none_plan(context: Context) -> None:
    try:
        context.service.initialize_scopes(None, {})  # type: ignore[arg-type]
    except ValueError as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# initialize_scopes - None available_resources (line 95)
# ---------------------------------------------------------------------------


@given("a simple two-project plan")
def step_simple_two_project_plan(context: Context) -> None:
    context.project_links = [
        ProjectLink(project_name="proj-a"),
        ProjectLink(project_name="proj-b"),
    ]
    context.plan = _make_plan(context.project_links)


@when("I call initialize_scopes with None available_resources")
def step_init_scopes_none_resources(context: Context) -> None:
    try:
        context.service.initialize_scopes(context.plan, None)  # type: ignore[arg-type]
    except ValueError as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# resolve_context_view - None plan (line 157)
# ---------------------------------------------------------------------------


@when('I call resolve_context_view with a None plan and project "{proj}"')
def step_resolve_ctx_none_plan(context: Context, proj: str) -> None:
    try:
        context.service.resolve_context_view(None, proj)  # type: ignore[arg-type]
    except ValueError as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# record_changeset - None plan (line 196)
# ---------------------------------------------------------------------------


@when("I call record_changeset with a None plan")
def step_record_changeset_none_plan(context: Context) -> None:
    summary = ChangeSetSummary(project_name="any", files_changed=1)
    try:
        context.service.record_changeset(None, "any", summary)  # type: ignore[arg-type]
    except ValueError as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# record_changeset - metadata not initialized (lines 198-199)
# ---------------------------------------------------------------------------


@when("I call record_changeset on a plan without metadata")
def step_record_changeset_no_metadata(context: Context) -> None:
    summary = ChangeSetSummary(project_name="proj-a", files_changed=1)
    try:
        context.service.record_changeset(context.plan, "proj-a", summary)
    except ValueError as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# validate_cross_project - None plan (line 253)
# ---------------------------------------------------------------------------


@when("I call validate_cross_project with a None plan")
def step_validate_none_plan(context: Context) -> None:
    context.validation_result = context.service.validate_cross_project(None)  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# validate_cross_project - no metadata (line 255)
# ---------------------------------------------------------------------------


@when("I call validate_cross_project on a plan without metadata")
def step_validate_no_metadata(context: Context) -> None:
    context.validation_result = context.service.validate_cross_project(context.plan)


# ---------------------------------------------------------------------------
# validate_cross_project - unknown source project (lines 283-285)
# ---------------------------------------------------------------------------


@given("the plan has initialized scopes with resources")
def step_init_scopes_with_resources(context: Context) -> None:
    resources = {"proj-a": ["r1"], "proj-b": ["r2"]}
    context.plan = context.service.initialize_scopes(context.plan, resources)


@given(
    'the plan has a cross-project dependency from unknown source "{src}" to known target "{tgt}"'
)
def step_add_unknown_source_dep(context: Context, src: str, tgt: str) -> None:
    dep = CrossProjectDependency(
        source_project=src,
        target_project=tgt,
        dependency_type="imports",
    )
    mp = context.plan.multi_project_metadata
    assert mp is not None, "metadata should be initialized before adding dependencies"
    updated_deps = [*list(mp.cross_project_dependencies), dep]
    updated_mp = mp.model_copy(update={"cross_project_dependencies": updated_deps})
    context.plan = context.plan.model_copy(
        update={"multi_project_metadata": updated_mp},
    )


@when("I call validate_cross_project on the prepared plan")
def step_validate_prepared(context: Context) -> None:
    context.validation_result = context.service.validate_cross_project(context.plan)


# ---------------------------------------------------------------------------
# Shared Then clauses
# ---------------------------------------------------------------------------


@then('a ValueError should have been raised with message "{fragment}"')
def step_check_value_error(context: Context, fragment: str) -> None:
    # Support two contexts: caught_exception (multi-project) and ontology_error (uko)
    err = getattr(context, "caught_exception", None)
    if err is None:
        err = getattr(context, "ontology_error", None)
    assert err is not None, "Expected a ValueError but none was raised"
    assert isinstance(err, ValueError), f"Expected ValueError, got {type(err).__name__}"
    assert fragment in str(err), f"Expected message containing '{fragment}', got: {err}"


@then('the validation result should contain "{fragment}"')
def step_check_validation_contains(context: Context, fragment: str) -> None:
    assert context.validation_result is not None, "validation_result is None"
    assert any(fragment in msg for msg in context.validation_result), (
        f"Expected a message containing '{fragment}' in {context.validation_result}"
    )


@then("the validation result should be an empty list")
def step_check_validation_empty(context: Context) -> None:
    assert context.validation_result is not None, "validation_result is None"
    assert context.validation_result == [], (
        f"Expected empty list, got {context.validation_result}"
    )
