"""Step definitions for acms_context_analysis_engine.feature."""

from __future__ import annotations

import json
from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]

from cleveragents.application.services.context_analysis_engine import (
    AnalysisResult,
    BudgetUtilization,
    ContextAnalysisEngine,
    TierDistribution,
    TierStats,
    TopFileEntry,
)
from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment

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


def _make_fragment(
    fragment_id: str,
    content: str,
    tier: ContextTier,
    resource_id: str = "",
    access_count: int = 0,
) -> TieredFragment:
    return TieredFragment(
        fragment_id=fragment_id,
        content=content,
        tier=tier,
        resource_id=resource_id,
        access_count=access_count,
    )


def _make_engine(
    tier_service: ContextTierService,
    max_total_size: int | None = None,
) -> ContextAnalysisEngine:
    return ContextAnalysisEngine(
        tier_service=tier_service,
        max_total_size=max_total_size,
    )


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("an empty ContextAnalysisEngine")
def step_empty_engine(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.engine = _make_engine(context.tier_service)


@given("an empty ContextAnalysisEngine with max_total_size {size:d}")
def step_empty_engine_with_max(context: Context, size: int) -> None:
    context.tier_service = ContextTierService()
    context.engine = _make_engine(context.tier_service, max_total_size=size)


@given("an empty ContextAnalysisEngine without explicit max_total_size")
def step_empty_engine_no_max(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.engine = ContextAnalysisEngine(tier_service=context.tier_service)


@given("a ContextAnalysisEngine with fragments in all tiers")
def step_engine_all_tiers(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(
        _make_fragment("hot-1", "hot content", ContextTier.HOT, access_count=5)
    )
    context.tier_service.store(
        _make_fragment("warm-1", "warm content", ContextTier.WARM, access_count=2)
    )
    context.tier_service.store(
        _make_fragment("cold-1", "cold content", ContextTier.COLD, access_count=1)
    )
    context.engine = _make_engine(context.tier_service, max_total_size=10000)


@given('a ContextAnalysisEngine with one hot fragment of content "hello"')
def step_engine_one_hot_hello(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(_make_fragment("hot-1", "hello", ContextTier.HOT))
    context.engine = _make_engine(context.tier_service)


@given('a ContextAnalysisEngine with one warm fragment of content "world"')
def step_engine_one_warm_world(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(_make_fragment("warm-1", "world", ContextTier.WARM))
    context.engine = _make_engine(context.tier_service)


@given('a ContextAnalysisEngine with one cold fragment of content "cold"')
def step_engine_one_cold_cold(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(_make_fragment("cold-1", "cold", ContextTier.COLD))
    context.engine = _make_engine(context.tier_service)


@given('a ContextAnalysisEngine with two hot fragments of content "ab" and "cde"')
def step_engine_two_hot_ab_cde(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(_make_fragment("hot-1", "ab", ContextTier.HOT))
    context.tier_service.store(_make_fragment("hot-2", "cde", ContextTier.HOT))
    context.engine = _make_engine(context.tier_service)


@given(
    'a ContextAnalysisEngine with a hot fragment of content "hello" and max_total_size 10'
)
def step_engine_hot_hello_max10(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(_make_fragment("hot-1", "hello", ContextTier.HOT))
    context.engine = _make_engine(context.tier_service, max_total_size=10)


@given(
    'a ContextAnalysisEngine with a hot fragment of content "hello world" and max_total_size 5'
)
def step_engine_hot_hello_world_max5(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(_make_fragment("hot-1", "hello world", ContextTier.HOT))
    context.engine = _make_engine(context.tier_service, max_total_size=5)


@given(
    "a ContextAnalysisEngine with fragments having access counts {a:d} and {b:d} and {c:d}"
)
def step_engine_access_counts(context: Context, a: int, b: int, c: int) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(
        _make_fragment("frag-a", "content a", ContextTier.HOT, access_count=a)
    )
    context.tier_service.store(
        _make_fragment("frag-b", "content b", ContextTier.WARM, access_count=b)
    )
    context.tier_service.store(
        _make_fragment("frag-c", "content c", ContextTier.COLD, access_count=c)
    )
    context.engine = _make_engine(context.tier_service)


@given(
    'a ContextAnalysisEngine with one hot fragment with resource_id "uko:file/main.py" and access_count 3'
)
def step_engine_hot_resource_main(context: Context) -> None:
    context.tier_service = ContextTierService()
    context.tier_service.store(
        _make_fragment(
            "hot-1",
            "content",
            ContextTier.HOT,
            resource_id="uko:file/main.py",
            access_count=3,
        )
    )
    context.engine = _make_engine(context.tier_service)


@given("a TierStats with count {count:d} and size_bytes {size:d}")
def step_tier_stats(context: Context, count: int, size: int) -> None:
    context.tier_stats = TierStats(count=count, size_bytes=size)


@given("a BudgetUtilization with current {current:d} max {max_b:d} pct {pct:f}")
def step_budget_util(context: Context, current: int, max_b: int, pct: float) -> None:
    context.budget_util = BudgetUtilization(
        current_bytes=current,
        max_bytes=max_b,
        utilization_pct=pct,
    )


@given(
    'a TopFileEntry with fragment_id "f1" resource_id "r1" access_count 5 tier "hot"'
)
def step_top_file_entry(context: Context) -> None:
    context.top_file_entry = TopFileEntry(
        fragment_id="f1",
        resource_id="r1",
        access_count=5,
        tier="hot",
    )


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when("I call entry_count")
def step_call_entry_count(context: Context) -> None:
    context.result = context.engine.entry_count()


@when("I call tier_distribution")
def step_call_tier_distribution(context: Context) -> None:
    context.result = context.engine.tier_distribution()


@when("I call budget_utilization")
def step_call_budget_utilization(context: Context) -> None:
    context.result = context.engine.budget_utilization()


@when("I call top_files with n {n:d}")
def step_call_top_files(context: Context, n: int) -> None:
    context.raised_error = None
    try:
        context.result = context.engine.top_files(n=n)
    except ValueError as exc:
        context.raised_error = exc


@when("I call analyze with top_n {top_n:d}")
def step_call_analyze(context: Context, top_n: int) -> None:
    context.analysis_result = context.engine.analyze(top_n=top_n)


@when("I format the result as JSON")
def step_format_json(context: Context) -> None:
    context.acms_json_output = ContextAnalysisEngine.format_json(
        context.analysis_result
    )
    context.acms_json_data = json.loads(context.acms_json_output)


@when("I format the result as text")
def step_format_text(context: Context) -> None:
    context.text_output = ContextAnalysisEngine.format_text(context.analysis_result)


@when("I call to_dict on TierStats")
def step_tier_stats_to_dict(context: Context) -> None:
    context.result = context.tier_stats.to_dict()


@when("I call to_dict on BudgetUtilization")
def step_budget_util_to_dict(context: Context) -> None:
    context.result = context.budget_util.to_dict()


@when("I call to_dict on TopFileEntry")
def step_top_file_to_dict(context: Context) -> None:
    context.result = context.top_file_entry.to_dict()


@when("I invoke the context analyze CLI command with empty tier service")
def step_invoke_cli_analyze_empty(context: Context) -> None:
    from typer.testing import CliRunner

    from cleveragents.cli.commands.context import app

    runner = CliRunner()
    result = runner.invoke(app, ["analyze"])
    context.cli_result = result
    context.cli_output = result.output


@when(
    "I invoke the context analyze CLI command with empty tier service and format json"
)
def step_invoke_cli_analyze_empty_json(context: Context) -> None:
    from typer.testing import CliRunner

    from cleveragents.cli.commands.context import app

    runner = CliRunner()
    result = runner.invoke(app, ["analyze", "--format", "json"])
    context.cli_result = result
    context.cli_output = result.output
    context.acms_cli_json_data = json.loads(result.output)


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then("the entry count should be {expected:d}")
def step_assert_entry_count(context: Context, expected: int) -> None:
    assert context.result == expected, (
        f"Expected entry count {expected}, got {context.result}"
    )


@then("the hot tier count should be {expected:d}")
def step_assert_hot_count(context: Context, expected: int) -> None:
    dist: TierDistribution = context.result
    assert dist.hot.count == expected, (
        f"Expected hot count {expected}, got {dist.hot.count}"
    )


@then("the warm tier count should be {expected:d}")
def step_assert_warm_count(context: Context, expected: int) -> None:
    dist: TierDistribution = context.result
    assert dist.warm.count == expected, (
        f"Expected warm count {expected}, got {dist.warm.count}"
    )


@then("the cold tier count should be {expected:d}")
def step_assert_cold_count(context: Context, expected: int) -> None:
    dist: TierDistribution = context.result
    assert dist.cold.count == expected, (
        f"Expected cold count {expected}, got {dist.cold.count}"
    )


@then("the hot tier size_bytes should be {expected:d}")
def step_assert_hot_size(context: Context, expected: int) -> None:
    dist: TierDistribution = context.result
    assert dist.hot.size_bytes == expected, (
        f"Expected hot size_bytes {expected}, got {dist.hot.size_bytes}"
    )


@then("the warm tier size_bytes should be {expected:d}")
def step_assert_warm_size(context: Context, expected: int) -> None:
    dist: TierDistribution = context.result
    assert dist.warm.size_bytes == expected, (
        f"Expected warm size_bytes {expected}, got {dist.warm.size_bytes}"
    )


@then("the cold tier size_bytes should be {expected:d}")
def step_assert_cold_size(context: Context, expected: int) -> None:
    dist: TierDistribution = context.result
    assert dist.cold.size_bytes == expected, (
        f"Expected cold size_bytes {expected}, got {dist.cold.size_bytes}"
    )


@then("the current_bytes should be {expected:d}")
def step_assert_current_bytes(context: Context, expected: int) -> None:
    util: BudgetUtilization = context.result
    assert util.current_bytes == expected, (
        f"Expected current_bytes {expected}, got {util.current_bytes}"
    )


@then("the max_bytes should be {expected:d}")
def step_assert_max_bytes(context: Context, expected: int) -> None:
    util: BudgetUtilization = context.result
    assert util.max_bytes == expected, (
        f"Expected max_bytes {expected}, got {util.max_bytes}"
    )


@then("the utilization_pct should be {expected:f}")
def step_assert_utilization_pct(context: Context, expected: float) -> None:
    util: BudgetUtilization = context.result
    assert abs(util.utilization_pct - expected) < 0.01, (
        f"Expected utilization_pct {expected}, got {util.utilization_pct}"
    )


@then("the top files list should be empty")
def step_assert_top_files_empty(context: Context) -> None:
    assert context.result == [], f"Expected empty list, got {context.result}"


@then("the top files should be ordered by access_count descending")
def step_assert_top_files_ordered(context: Context) -> None:
    files: list[TopFileEntry] = context.result
    counts = [f.access_count for f in files]
    assert counts == sorted(counts, reverse=True), (
        f"Expected descending order, got {counts}"
    )


@then("the top files list should have {expected:d} entries")
def step_assert_top_files_count(context: Context, expected: int) -> None:
    assert len(context.result) == expected, (
        f"Expected {expected} entries, got {len(context.result)}"
    )


@then("a ValueError should be raised for top_files n")
def step_assert_value_error_top_files(context: Context) -> None:
    assert context.raised_error is not None, "Expected ValueError but none was raised"
    assert isinstance(context.raised_error, ValueError)


@then('the first top file should have resource_id "uko:file/main.py"')
def step_assert_first_resource_id(context: Context) -> None:
    files: list[TopFileEntry] = context.result
    assert len(files) > 0, "Expected at least one top file"
    assert files[0].resource_id == "uko:file/main.py", (
        f"Expected resource_id 'uko:file/main.py', got {files[0].resource_id!r}"
    )


@then("the first top file should have access_count {expected:d}")
def step_assert_first_access_count(context: Context, expected: int) -> None:
    files: list[TopFileEntry] = context.result
    assert files[0].access_count == expected, (
        f"Expected access_count {expected}, got {files[0].access_count}"
    )


@then('the first top file should have tier "hot"')
def step_assert_first_tier_hot(context: Context) -> None:
    files: list[TopFileEntry] = context.result
    assert files[0].tier == "hot", f"Expected tier 'hot', got {files[0].tier!r}"


@then("the analysis entry_count should be {expected:d}")
def step_assert_analysis_entry_count(context: Context, expected: int) -> None:
    result: AnalysisResult = context.analysis_result
    assert result.entry_count == expected, (
        f"Expected entry_count {expected}, got {result.entry_count}"
    )


@then("the analysis tier_distribution should have hot count {expected:d}")
def step_assert_analysis_hot_count(context: Context, expected: int) -> None:
    result: AnalysisResult = context.analysis_result
    assert result.tier_distribution.hot.count == expected, (
        f"Expected hot count {expected}, got {result.tier_distribution.hot.count}"
    )


@then("the analysis top_files should not be empty")
def step_assert_analysis_top_files_not_empty(context: Context) -> None:
    result: AnalysisResult = context.analysis_result
    assert len(result.top_files) > 0, "Expected non-empty top_files"


@then('the acms JSON output should contain key "entry_count"')
def step_assert_acms_json_entry_count(context: Context) -> None:
    assert "entry_count" in context.acms_json_data, (
        "Expected key 'entry_count' in JSON output"
    )


@then('the acms JSON output should contain key "tier_distribution"')
def step_assert_acms_json_tier_dist(context: Context) -> None:
    assert "tier_distribution" in context.acms_json_data, (
        "Expected key 'tier_distribution' in JSON output"
    )


@then('the acms JSON output should contain key "budget_utilization"')
def step_assert_acms_json_budget(context: Context) -> None:
    assert "budget_utilization" in context.acms_json_data, (
        "Expected key 'budget_utilization' in JSON output"
    )


@then('the acms JSON output should contain key "top_files"')
def step_assert_acms_json_top_files(context: Context) -> None:
    assert "top_files" in context.acms_json_data, (
        "Expected key 'top_files' in JSON output"
    )


@then('the acms JSON tier_distribution should have keys "hot" "warm" "cold"')
def step_assert_acms_json_tier_keys(context: Context) -> None:
    tier_dist = context.acms_json_data.get("tier_distribution", {})
    for key in ("hot", "warm", "cold"):
        assert key in tier_dist, (
            f"Expected key {key!r} in tier_distribution, got: {list(tier_dist.keys())}"
        )


@then(
    'the acms JSON budget_utilization should have keys "current_bytes" "max_bytes" "utilization_pct"'
)
def step_assert_acms_json_budget_keys(context: Context) -> None:
    budget = context.acms_json_data.get("budget_utilization", {})
    for key in ("current_bytes", "max_bytes", "utilization_pct"):
        assert key in budget, (
            f"Expected key {key!r} in budget_utilization, got: {list(budget.keys())}"
        )


@then('the text output should contain "ACMS Context Analysis"')
def step_assert_text_acms(context: Context) -> None:
    assert "ACMS Context Analysis" in context.text_output, (
        f"Expected 'ACMS Context Analysis' in text output:\n{context.text_output}"
    )


@then('the text output should contain "Tier Distribution"')
def step_assert_text_tier_dist(context: Context) -> None:
    assert "Tier Distribution" in context.text_output, (
        f"Expected 'Tier Distribution' in text output:\n{context.text_output}"
    )


@then('the text output should contain "Budget Utilization"')
def step_assert_text_budget(context: Context) -> None:
    assert "Budget Utilization" in context.text_output, (
        f"Expected 'Budget Utilization' in text output:\n{context.text_output}"
    )


@then('the text output should contain "Top"')
def step_assert_text_top(context: Context) -> None:
    assert "Top" in context.text_output, (
        f"Expected 'Top' in text output:\n{context.text_output}"
    )


@then('the text output should contain "(no entries)"')
def step_assert_text_no_entries(context: Context) -> None:
    assert "(no entries)" in context.text_output, (
        f"Expected '(no entries)' in text output:\n{context.text_output}"
    )


@then('the result to_dict should contain key "entry_count"')
def step_assert_result_dict_entry_count(context: Context) -> None:
    d = context.analysis_result.to_dict()
    assert "entry_count" in d, "Expected key 'entry_count' in result dict"


@then('the result to_dict should contain key "tier_distribution"')
def step_assert_result_dict_tier_dist(context: Context) -> None:
    d = context.analysis_result.to_dict()
    assert "tier_distribution" in d, "Expected key 'tier_distribution' in result dict"


@then('the result to_dict should contain key "budget_utilization"')
def step_assert_result_dict_budget(context: Context) -> None:
    d = context.analysis_result.to_dict()
    assert "budget_utilization" in d, "Expected key 'budget_utilization' in result dict"


@then('the result to_dict should contain key "top_files"')
def step_assert_result_dict_top_files(context: Context) -> None:
    d = context.analysis_result.to_dict()
    assert "top_files" in d, "Expected key 'top_files' in result dict"


@then("the TierStats dict should have count {count:d} and size_bytes {size:d}")
def step_assert_tier_stats_dict(context: Context, count: int, size: int) -> None:
    d: dict[str, int] = context.result
    assert d["count"] == count, f"Expected count {count}, got {d['count']}"
    assert d["size_bytes"] == size, f"Expected size_bytes {size}, got {d['size_bytes']}"


@then("the BudgetUtilization dict utilization_pct should be {expected:f}")
def step_assert_budget_util_dict_pct(context: Context, expected: float) -> None:
    d: dict[str, Any] = context.result
    assert abs(d["utilization_pct"] - expected) < 0.01, (
        f"Expected utilization_pct {expected}, got {d['utilization_pct']}"
    )


@then("the TopFileEntry dict should have all fields")
def step_assert_top_file_dict(context: Context) -> None:
    d: dict[str, Any] = context.result
    for key in ("fragment_id", "resource_id", "access_count", "tier"):
        assert key in d, f"Expected key {key!r} in TopFileEntry dict"


@then("the max_bytes should be the hot-tier budget")
def step_assert_max_bytes_hot_budget(context: Context) -> None:
    util: BudgetUtilization = context.result
    expected = context.tier_service.budget.max_tokens_hot
    assert util.max_bytes == expected, (
        f"Expected max_bytes {expected}, got {util.max_bytes}"
    )


@then('the CLI output should contain "ACMS Context Analysis"')
def step_assert_cli_output_acms(context: Context) -> None:
    assert "ACMS Context Analysis" in context.cli_output, (
        f"Expected 'ACMS Context Analysis' in CLI output:\n{context.cli_output}"
    )


@then('the acms CLI JSON output should contain key "entry_count"')
def step_assert_acms_cli_json_key(context: Context) -> None:
    assert "entry_count" in context.acms_cli_json_data, (
        "Expected key 'entry_count' in CLI JSON output"
    )
