"""Step definitions for cost controls feature tests."""

from __future__ import annotations

import os
import threading
from typing import Any

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

# ---- CostEntry steps ----


@given("I import the CostEntry class")
def step_import_cost_entry(context: Context) -> None:
    from cleveragents.providers.cost_table import CostEntry

    context.CostEntry = CostEntry


@when("I create a CostEntry with negative input cost")
def step_create_cost_entry_negative_input(context: Context) -> None:
    context.cost_entry_error = None
    try:
        context.CostEntry(input_cost_per_token=-0.001, output_cost_per_token=0.001)
    except ValueError as exc:
        context.cost_entry_error = exc


@when("I create a CostEntry with negative output cost")
def step_create_cost_entry_negative_output(context: Context) -> None:
    context.cost_entry_error = None
    try:
        context.CostEntry(input_cost_per_token=0.001, output_cost_per_token=-0.001)
    except ValueError as exc:
        context.cost_entry_error = exc


@then("a ValueError should be raised for CostEntry")
def step_cost_entry_valueerror(context: Context) -> None:
    assert context.cost_entry_error is not None, "Expected ValueError"
    assert isinstance(context.cost_entry_error, ValueError)


@when(
    "I create a CostEntry with input cost {input_cost:f} and output cost {output_cost:f}"
)
def step_create_cost_entry_with_costs(
    context: Context, input_cost: float, output_cost: float
) -> None:
    context.cost_entry = context.CostEntry(
        input_cost_per_token=input_cost,
        output_cost_per_token=output_cost,
    )


@when(
    "I estimate cost for {input_tokens:d} input tokens and {output_tokens:d} output tokens"
)
def step_estimate_cost(context: Context, input_tokens: int, output_tokens: int) -> None:
    context.estimated_cost = context.cost_entry.estimate_cost(
        input_tokens, output_tokens
    )


@then("the estimated cost should be {expected:f}")
def step_check_estimated_cost(context: Context, expected: float) -> None:
    assert abs(context.estimated_cost - expected) < 1e-9, (
        f"Expected {expected}, got {context.estimated_cost}"
    )


@then("estimating cost with negative input tokens raises ValueError")
def step_estimate_negative_input(context: Context) -> None:
    try:
        context.cost_entry.estimate_cost(-1, 0)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("estimating cost with negative output tokens raises ValueError")
def step_estimate_negative_output(context: Context) -> None:
    try:
        context.cost_entry.estimate_cost(0, -1)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---- ProviderCostTable steps ----


@given("I create a ProviderCostTable with defaults")
def step_create_default_cost_table(context: Context) -> None:
    from cleveragents.providers.cost_table import ProviderCostTable

    context.cost_table = ProviderCostTable()


@when('I look up cost for provider "{provider}" model "{model}"')
def step_lookup_cost(context: Context, provider: str, model: str) -> None:
    context.cost_entry_result = context.cost_table.get_cost_entry(provider, model)


@then("the cost entry should be the default cost")
def step_check_default_cost(context: Context) -> None:
    from cleveragents.providers.cost_table import ProviderCostTable

    assert context.cost_entry_result == ProviderCostTable.DEFAULT_COST


@then("the cost entry input cost should be {expected:f}")
def step_check_input_cost(context: Context, expected: float) -> None:
    assert abs(context.cost_entry_result.input_cost_per_token - expected) < 1e-12, (
        f"Expected {expected}, got {context.cost_entry_result.input_cost_per_token}"
    )


@then("the cost entry output cost should be {expected:f}")
def step_check_output_cost(context: Context, expected: float) -> None:
    assert abs(context.cost_entry_result.output_cost_per_token - expected) < 1e-12, (
        f"Expected {expected}, got {context.cost_entry_result.output_cost_per_token}"
    )


@given('I create a ProviderCostTable with custom entry for "{provider}" "{model}"')
def step_create_custom_cost_table(context: Context, provider: str, model: str) -> None:
    from cleveragents.providers.cost_table import CostEntry, ProviderCostTable

    custom: dict[str, dict[str, CostEntry]] = {
        provider: {
            model: CostEntry(input_cost_per_token=0.001, output_cost_per_token=0.002)
        }
    }
    context.cost_table = ProviderCostTable(custom_entries=custom)


@then("looking up cost for empty provider raises ValueError")
def step_lookup_empty_provider(context: Context) -> None:
    try:
        context.cost_table.get_cost_entry("", "model")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("looking up cost for empty model raises ValueError")
def step_lookup_empty_model(context: Context) -> None:
    try:
        context.cost_table.get_cost_entry("openai", "")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@when("I list providers from the cost table")
def step_list_providers(context: Context) -> None:
    context.provider_list = context.cost_table.list_providers()


@then('the provider list should include "{provider}"')
def step_check_provider_in_list(context: Context, provider: str) -> None:
    assert provider in context.provider_list, (
        f"{provider} not in {context.provider_list}"
    )


@when('I list models for provider "{provider}"')
def step_list_models(context: Context, provider: str) -> None:
    context.model_list = context.cost_table.list_models(provider)


@then('the model list should include "{model}"')
def step_check_model_in_list(context: Context, model: str) -> None:
    assert model in context.model_list, f"{model} not in {context.model_list}"


@when("I create a ProviderCostTable with non-dict custom entries")
def step_create_cost_table_non_dict(context: Context) -> None:
    from cleveragents.providers.cost_table import ProviderCostTable

    context.cost_table_error = None
    try:
        ProviderCostTable(custom_entries="not-a-dict")  # type: ignore[arg-type]
    except TypeError as exc:
        context.cost_table_error = exc


@then("a TypeError should be raised for ProviderCostTable")
def step_cost_table_typeerror(context: Context) -> None:
    assert context.cost_table_error is not None, "Expected TypeError"
    assert isinstance(context.cost_table_error, TypeError)


# ---- CostMetadata steps ----


@given("I create a fresh CostMetadata")
def step_create_cost_metadata(context: Context) -> None:
    from cleveragents.domain.models.core.cost_metadata import CostMetadata

    context.cost_metadata = CostMetadata()


@when(
    "I record usage of {input_t:d} input tokens and {output_t:d} output tokens "
    'at cost {cost:f} for provider "{provider}"'
)
def step_record_usage(
    context: Context, input_t: int, output_t: int, cost: float, provider: str
) -> None:
    context.cost_metadata.record_usage(
        input_tokens=input_t,
        output_tokens=output_t,
        cost=cost,
        provider=provider,
    )


@then("total tokens should be {expected:d}")
def step_check_total_tokens(context: Context, expected: int) -> None:
    assert context.cost_metadata.total_tokens == expected, (
        f"Expected {expected}, got {context.cost_metadata.total_tokens}"
    )


@then("total cost should be {expected:f}")
def step_check_total_cost(context: Context, expected: float) -> None:
    assert abs(context.cost_metadata.total_cost - expected) < 1e-9, (
        f"Expected {expected}, got {context.cost_metadata.total_cost}"
    )


@then('provider costs for "{provider}" should be {expected:f}')
def step_check_provider_costs(context: Context, provider: str, expected: float) -> None:
    actual = context.cost_metadata.provider_costs.get(provider, 0.0)
    assert abs(actual - expected) < 1e-9, f"Expected {expected}, got {actual}"


@then("recording usage with negative input tokens raises ValueError")
def step_record_negative_input(context: Context) -> None:
    try:
        context.cost_metadata.record_usage(
            input_tokens=-1, output_tokens=0, cost=0.0, provider="test"
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording usage with negative output tokens raises ValueError")
def step_record_negative_output_tokens(context: Context) -> None:
    try:
        context.cost_metadata.record_usage(
            input_tokens=0, output_tokens=-1, cost=0.0, provider="test"
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording usage with negative cost raises ValueError")
def step_record_negative_cost(context: Context) -> None:
    try:
        context.cost_metadata.record_usage(
            input_tokens=0, output_tokens=0, cost=-1.0, provider="test"
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording usage with empty provider raises ValueError")
def step_record_empty_provider(context: Context) -> None:
    try:
        context.cost_metadata.record_usage(
            input_tokens=0, output_tokens=0, cost=0.0, provider=""
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@when("I get the display dict from cost metadata")
def step_get_display_dict(context: Context) -> None:
    context.display_dict = context.cost_metadata.as_display_dict()


@then('the display dict should have key "{key}"')
def step_check_display_dict_key(context: Context, key: str) -> None:
    assert key in context.display_dict, f"Key {key} not in {context.display_dict}"


@when("I set budget remaining to {value:f}")
def step_set_budget_remaining(context: Context, value: float) -> None:
    context.cost_metadata.budget_remaining = value


# ---- BudgetExhaustionEvent steps ----


@given("I import the BudgetExhaustionEvent class")
def step_import_exhaustion_event(context: Context) -> None:
    from cleveragents.domain.models.core.cost_metadata import BudgetExhaustionEvent

    context.BudgetExhaustionEvent = BudgetExhaustionEvent


@when("I create a BudgetExhaustionEvent with invalid budget type")
def step_create_invalid_event(context: Context) -> None:
    context.exhaustion_event_error = None
    try:
        context.BudgetExhaustionEvent(budget_type="invalid", limit=10.0, used=5.0)
    except Exception as exc:
        context.exhaustion_event_error = exc


@then("a validation error should be raised for BudgetExhaustionEvent")
def step_check_exhaustion_validation_error(context: Context) -> None:
    assert context.exhaustion_event_error is not None, "Expected validation error"


@when('I create a BudgetExhaustionEvent with budget type "{btype}"')
def step_create_valid_event(context: Context, btype: str) -> None:
    context.exhaustion_event = context.BudgetExhaustionEvent(
        budget_type=btype, limit=10.0, used=5.0
    )


@then("the event should be created successfully")
def step_check_event_created(context: Context) -> None:
    assert context.exhaustion_event is not None


# ---- CostTracker steps ----


@given("I create a CostTracker with no limits")
def step_create_tracker_no_limits(context: Context) -> None:
    from cleveragents.providers.cost_tracker import CostTracker

    context.cost_tracker = CostTracker()


@given("I create a CostTracker with plan budget {budget:f}")
def step_create_tracker_plan_budget(context: Context, budget: float) -> None:
    from cleveragents.providers.cost_tracker import CostTracker

    context.cost_tracker = CostTracker(budget_per_plan=budget)


@given("I create a CostTracker with daily budget {budget:f}")
def step_create_tracker_daily_budget(context: Context, budget: float) -> None:
    from cleveragents.providers.cost_tracker import CostTracker

    context.cost_tracker = CostTracker(budget_per_day=budget)
    context.fallback_cost_tracker = context.cost_tracker


@given("I create a CostTracker with plan budget {plan:f} and daily budget {daily:f}")
def step_create_tracker_both_budgets(
    context: Context, plan: float, daily: float
) -> None:
    from cleveragents.providers.cost_tracker import CostTracker

    context.cost_tracker = CostTracker(budget_per_plan=plan, budget_per_day=daily)


@when(
    "I record tracked usage of {inp:d} input and {out:d} output "
    'for "{provider}" "{model}"'
)
def step_record_tracked_usage(
    context: Context, inp: int, out: int, provider: str, model: str
) -> None:
    if not hasattr(context, "cost_metadata"):
        from cleveragents.domain.models.core.cost_metadata import CostMetadata

        context.cost_metadata = CostMetadata()

    context.budget_result = context.cost_tracker.record_usage(
        context.cost_metadata,
        provider=provider,
        model=model,
        input_tokens=inp,
        output_tokens=out,
    )


@then("the budget check should be under budget")
def step_check_under_budget(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.UNDER_BUDGET, (
        f"Expected UNDER_BUDGET, got {context.budget_result.status}"
    )


@then("the budget check should be warning or exceeded")
def step_check_warning_or_exceeded(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status in (
        BudgetStatus.WARNING,
        BudgetStatus.EXCEEDED,
    ), f"Expected WARNING or EXCEEDED, got {context.budget_result.status}"


@then("the budget check should be exceeded")
def step_check_exceeded(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.EXCEEDED, (
        f"Expected EXCEEDED, got {context.budget_result.status}"
    )


@then("the cost metadata total tokens should be {expected:d}")
def step_check_metadata_total_tokens(context: Context, expected: int) -> None:
    assert context.cost_metadata.total_tokens == expected, (
        f"Expected {expected}, got {context.cost_metadata.total_tokens}"
    )


@then("the cost metadata should have budget exhaustion events")
def step_check_exhaustion_events(context: Context) -> None:
    assert len(context.cost_metadata.budget_exhaustion_events) > 0, (
        "Expected budget exhaustion events"
    )


@when("I create a CostTracker with negative plan budget")
def step_create_tracker_negative_plan(context: Context) -> None:
    from cleveragents.providers.cost_tracker import CostTracker

    context.tracker_error = None
    try:
        CostTracker(budget_per_plan=-1.0)
    except ValueError as exc:
        context.tracker_error = exc


@when("I create a CostTracker with negative daily budget")
def step_create_tracker_negative_daily(context: Context) -> None:
    from cleveragents.providers.cost_tracker import CostTracker

    context.tracker_error = None
    try:
        CostTracker(budget_per_day=-1.0)
    except ValueError as exc:
        context.tracker_error = exc


@then("a ValueError should be raised for CostTracker")
def step_check_tracker_valueerror(context: Context) -> None:
    assert context.tracker_error is not None, "Expected ValueError"
    assert isinstance(context.tracker_error, ValueError)


@when(
    'I estimate cost for "{provider}" "{model}" with {inp:d} input and {out:d} output'
)
def step_estimate_tracker_cost(
    context: Context, provider: str, model: str, inp: int, out: int
) -> None:
    context.tracker_estimated_cost = context.cost_tracker.estimate_cost(
        provider, model, inp, out
    )


@then("the estimated tracker cost should be positive")
def step_check_tracker_positive_cost(context: Context) -> None:
    assert context.tracker_estimated_cost > 0.0, (
        f"Expected positive cost, got {context.tracker_estimated_cost}"
    )


@then("estimating cost with empty provider raises ValueError on tracker")
def step_estimate_empty_provider_tracker(context: Context) -> None:
    try:
        context.cost_tracker.estimate_cost("", "gpt-4o", 100, 50)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("estimating cost with empty model raises ValueError on tracker")
def step_estimate_empty_model_tracker(context: Context) -> None:
    try:
        context.cost_tracker.estimate_cost("openai", "", 100, 50)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording tracked usage with negative input raises ValueError")
def step_record_tracked_negative_input(context: Context) -> None:
    try:
        context.cost_tracker.record_usage(
            context.cost_metadata,
            provider="openai",
            model="gpt-4o",
            input_tokens=-1,
            output_tokens=0,
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@when("I check plan budget")
def step_check_plan_budget(context: Context) -> None:
    context.budget_result = context.cost_tracker.check_plan_budget(
        context.cost_metadata
    )


@when("I check daily budget")
def step_check_daily_budget(context: Context) -> None:
    context.budget_result = context.cost_tracker.check_daily_budget()


@then("the plan budget check should be under budget")
def step_plan_under_budget(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.UNDER_BUDGET


@then("the plan budget check should be exceeded")
def step_plan_exceeded(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.EXCEEDED


@then("the daily budget check should be under budget")
def step_daily_under_budget(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.UNDER_BUDGET


@then("the daily budget check should be exceeded")
def step_daily_exceeded(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.EXCEEDED


@then("the daily spend should be {expected:f}")
def step_check_daily_spend(context: Context, expected: float) -> None:
    actual = context.cost_tracker.get_daily_spend()
    assert abs(actual - expected) < 1e-9, f"Expected {expected}, got {actual}"


@then("the tracker plan budget should be {expected:f}")
def step_check_tracker_plan_budget(context: Context, expected: float) -> None:
    assert context.cost_tracker.budget_per_plan == expected


@then("the tracker daily budget should be {expected:f}")
def step_check_tracker_daily_budget(context: Context, expected: float) -> None:
    assert context.cost_tracker.budget_per_day == expected


@then("getting cost entry with empty provider raises ValueError")
def step_get_cost_entry_empty_provider(context: Context) -> None:
    try:
        context.cost_tracker.get_cost_entry("", "gpt-4o")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("getting cost entry with empty model raises ValueError")
def step_get_cost_entry_empty_model(context: Context) -> None:
    try:
        context.cost_tracker.get_cost_entry("openai", "")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording tracked usage with empty provider raises ValueError")
def step_record_tracked_empty_provider(context: Context) -> None:
    try:
        context.cost_tracker.record_usage(
            context.cost_metadata,
            provider="",
            model="gpt-4o",
            input_tokens=1,
            output_tokens=1,
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording tracked usage with empty model raises ValueError")
def step_record_tracked_empty_model(context: Context) -> None:
    try:
        context.cost_tracker.record_usage(
            context.cost_metadata,
            provider="openai",
            model="",
            input_tokens=1,
            output_tokens=1,
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---- CostTracker concurrency steps ----


@when(
    "{n:d} threads concurrently record usage of {inp:d} input and {out:d} output "
    'for "{provider}" "{model}"'
)
def step_concurrent_record_usage(
    context: Context, n: int, inp: int, out: int, provider: str, model: str
) -> None:
    from cleveragents.domain.models.core.cost_metadata import CostMetadata

    # Each thread gets its own CostMetadata to avoid data races on that object.
    # We only care about the shared _daily_costs in CostTracker.
    errors: list[Exception] = []
    barrier = threading.Barrier(n)

    def worker() -> None:
        meta = CostMetadata()
        try:
            barrier.wait()  # All threads start simultaneously
            context.cost_tracker.record_usage(
                meta,
                provider=provider,
                model=model,
                input_tokens=inp,
                output_tokens=out,
            )
        except Exception as exc:
            errors.append(exc)

    threads = [threading.Thread(target=worker) for _ in range(n)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert not errors, f"Threads raised exceptions: {errors}"

    # Store expected total for the assertion step
    single_cost = context.cost_tracker.estimate_cost(provider, model, inp, out)
    context.expected_concurrent_total = single_cost * n


@then("the daily spend should equal the sum of all concurrent costs")
def step_check_concurrent_daily_spend(context: Context) -> None:
    actual = context.cost_tracker.get_daily_spend()
    expected = context.expected_concurrent_total
    assert abs(actual - expected) < 1e-9, (
        f"Expected daily spend {expected:.6f}, got {actual:.6f} — "
        "possible race condition: cost increments were lost"
    )


# ---- FallbackSelector steps ----


@given("I create a ProviderRegistry with no API keys for fallback")
def step_create_registry_no_keys(context: Context) -> None:
    import os

    from cleveragents.config.settings import Settings
    from cleveragents.providers.registry import ProviderRegistry

    # Temporarily clear all provider API keys to simulate no configured providers
    key_env_vars = [
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "GOOGLE_API_KEY",
        "AZURE_OPENAI_API_KEY",
        "OPENROUTER_API_KEY",
        "GEMINI_API_KEY",
        "GOOGLE_GENAI_API_KEY",
        "GROQ_API_KEY",
        "TOGETHER_API_KEY",
        "COHERE_API_KEY",
    ]
    saved: dict[str, str | None] = {}
    for var in key_env_vars:
        saved[var] = os.environ.pop(var, None)
    try:
        settings = Settings()
        context.fallback_registry = ProviderRegistry(settings)
    finally:
        for var, val in saved.items():
            if val is not None:
                os.environ[var] = val


@given("I create a ProviderRegistry with mock provider for fallback")
def step_create_registry_mock(context: Context) -> None:
    import os

    from cleveragents.config.settings import Settings
    from cleveragents.providers.registry import ProviderRegistry

    # Clear all real API keys, the mock provider is always discovered
    key_env_vars = [
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "GOOGLE_API_KEY",
        "AZURE_OPENAI_API_KEY",
        "OPENROUTER_API_KEY",
        "GEMINI_API_KEY",
        "GOOGLE_GENAI_API_KEY",
        "GROQ_API_KEY",
        "TOGETHER_API_KEY",
        "COHERE_API_KEY",
    ]
    saved: dict[str, str | None] = {}
    for var in key_env_vars:
        saved[var] = os.environ.pop(var, None)
    try:
        settings = Settings()
        context.fallback_registry = ProviderRegistry(settings)
    finally:
        for var, val in saved.items():
            if val is not None:
                os.environ[var] = val


@given("I create a ProviderRegistry with openai key for fallback")
def step_create_registry_openai(context: Context) -> None:
    from cleveragents.config.settings import Settings
    from cleveragents.providers.registry import ProviderRegistry

    old_key = os.environ.get("OPENAI_API_KEY")
    os.environ["OPENAI_API_KEY"] = "test-key-for-fallback"
    try:
        settings = Settings()
        context.fallback_registry = ProviderRegistry(settings)
    finally:
        if old_key is None:
            os.environ.pop("OPENAI_API_KEY", None)
        else:
            os.environ["OPENAI_API_KEY"] = old_key


@when("I create a FallbackSelector with the registry")
def step_create_fallback_selector(context: Context) -> None:
    from cleveragents.providers.fallback_selector import FallbackSelector

    tracker = getattr(context, "fallback_cost_tracker", None)
    context.fallback_selector = FallbackSelector(
        registry=context.fallback_registry,
        cost_tracker=tracker,
    )


@when("I select a fallback provider")
def step_select_fallback(context: Context) -> None:
    context.fallback_result = context.fallback_selector.select()


@when("I select a fallback provider requiring tool calls")
def step_select_fallback_tool_calls(context: Context) -> None:
    context.fallback_result = context.fallback_selector.select(require_tool_calls=True)


@then("the fallback result provider should be None")
def step_check_fallback_none(context: Context) -> None:
    assert context.fallback_result.provider_type is None, (
        f"Expected None, got {context.fallback_result.provider_type}"
    )


@then("the fallback result should have skipped entries")
def step_check_fallback_skipped(context: Context) -> None:
    assert len(context.fallback_result.skipped) > 0


@then("the fallback result should skip mock for missing tool calls")
def step_check_mock_skipped_tool_calls(context: Context) -> None:
    # All configured providers were skipped (none configured or missing caps)
    assert len(context.fallback_result.skipped) > 0


@then('the fallback result provider type should be "{expected}"')
def step_check_fallback_provider_type(context: Context, expected: str) -> None:
    assert context.fallback_result.provider_type is not None
    assert context.fallback_result.provider_type.value == expected, (
        f"Expected {expected}, got {context.fallback_result.provider_type.value}"
    )


@when('I create a FallbackSelector with custom order "{providers}"')
def step_create_fallback_custom_order(context: Context, providers: str) -> None:
    from cleveragents.providers.fallback_selector import FallbackSelector

    provider_list = [p.strip() for p in providers.split(",")]
    context.fallback_selector = FallbackSelector(
        registry=context.fallback_registry,
        fallback_providers=provider_list,
    )


@when("I create a FallbackSelector with None registry")
def step_create_fallback_none_registry(context: Context) -> None:
    from cleveragents.providers.fallback_selector import FallbackSelector

    context.fallback_error = None
    try:
        FallbackSelector(registry=None)  # type: ignore[arg-type]
    except TypeError as exc:
        context.fallback_error = exc


@then("a TypeError should be raised for FallbackSelector")
def step_check_fallback_typeerror(context: Context) -> None:
    assert context.fallback_error is not None, "Expected TypeError"
    assert isinstance(context.fallback_error, TypeError)


@when("I create a FallbackSelector with empty string in fallback list")
def step_create_fallback_empty_string(context: Context) -> None:
    from cleveragents.providers.fallback_selector import FallbackSelector

    context.fallback_error = None
    try:
        FallbackSelector(
            registry=context.fallback_registry,
            fallback_providers=["openai", ""],
        )
    except ValueError as exc:
        context.fallback_error = exc


@then("a ValueError should be raised for FallbackSelector")
def step_check_fallback_valueerror(context: Context) -> None:
    assert context.fallback_error is not None, "Expected ValueError"
    assert isinstance(context.fallback_error, ValueError)


@when("I create a FallbackSelector with cost tracker")
def step_create_fallback_with_tracker(context: Context) -> None:
    from cleveragents.providers.fallback_selector import FallbackSelector

    context.fallback_selector = FallbackSelector(
        registry=context.fallback_registry,
        cost_tracker=context.fallback_cost_tracker,
    )


# ---- Settings config keys ----


@when("I load cost control settings with defaults")
def step_load_default_settings(context: Context) -> None:
    from cleveragents.config.settings import Settings

    context.cost_settings = Settings()


@then("budget_per_plan should be None")
def step_check_budget_per_plan_none(context: Context) -> None:
    assert context.cost_settings.budget_per_plan is None


@then("budget_per_day should be None")
def step_check_budget_per_day_none(context: Context) -> None:
    assert context.cost_settings.budget_per_day is None


@then("fallback_providers should be an empty list")
def step_check_fallback_empty(context: Context) -> None:
    assert context.cost_settings.fallback_providers == []


@when("I load cost control settings with budget_per_plan {value:f}")
def step_load_settings_plan_budget(context: Context, value: float) -> None:
    from cleveragents.config.settings import Settings

    old = os.environ.get("CLEVERAGENTS_BUDGET_PER_PLAN")
    os.environ["CLEVERAGENTS_BUDGET_PER_PLAN"] = str(value)
    try:
        context.cost_settings = Settings()
    finally:
        if old is None:
            os.environ.pop("CLEVERAGENTS_BUDGET_PER_PLAN", None)
        else:
            os.environ["CLEVERAGENTS_BUDGET_PER_PLAN"] = old


@then("budget_per_plan should be {expected:f}")
def step_check_budget_per_plan_value(context: Context, expected: float) -> None:
    assert context.cost_settings.budget_per_plan == expected, (
        f"Expected {expected}, got {context.cost_settings.budget_per_plan}"
    )


@when("I load cost control settings with budget_per_day {value:f}")
def step_load_settings_daily_budget(context: Context, value: float) -> None:
    from cleveragents.config.settings import Settings

    old = os.environ.get("CLEVERAGENTS_BUDGET_PER_DAY")
    os.environ["CLEVERAGENTS_BUDGET_PER_DAY"] = str(value)
    try:
        context.cost_settings = Settings()
    finally:
        if old is None:
            os.environ.pop("CLEVERAGENTS_BUDGET_PER_DAY", None)
        else:
            os.environ["CLEVERAGENTS_BUDGET_PER_DAY"] = old


@then("budget_per_day should be {expected:f}")
def step_check_budget_per_day_value(context: Context, expected: float) -> None:
    assert context.cost_settings.budget_per_day == expected, (
        f"Expected {expected}, got {context.cost_settings.budget_per_day}"
    )


# ---- Plan model steps ----


def _make_minimal_plan(
    cost_meta: Any = None,
) -> Any:
    """Create a minimal Plan v3 model for testing."""
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        Plan,
        PlanIdentity,
    )

    return Plan(
        identity=PlanIdentity(plan_id="01ARZJG0J0000000000000000A"),
        namespaced_name=NamespacedName(namespace="local", name="test-plan"),
        description="Test plan for cost metadata",
        action_name="local/test-action",
        cost_metadata=cost_meta,
    )


@given("I create a minimal Plan with cost metadata")
def step_create_plan_with_cost(context: Context) -> None:
    from cleveragents.domain.models.core.cost_metadata import CostMetadata

    meta = CostMetadata(total_tokens=100, total_cost=0.01)
    context.plan_with_cost = _make_minimal_plan(cost_meta=meta)


@then("the plan should have cost_metadata set")
def step_check_plan_cost_metadata(context: Context) -> None:
    assert context.plan_with_cost.cost_metadata is not None


@when("I get the CLI dict from the plan")
def step_get_cli_dict(context: Context) -> None:
    plan = getattr(context, "plan_with_cost", None) or getattr(
        context, "plan_without_cost", None
    )
    context.cli_dict = plan.as_cli_dict()


@then('the CLI dict should have a "{key}" key')
def step_check_cli_dict_has_key(context: Context, key: str) -> None:
    assert key in context.cli_dict, f"Key {key} not in {list(context.cli_dict.keys())}"


@given("I create a minimal Plan without cost metadata")
def step_create_plan_without_cost(context: Context) -> None:
    context.plan_without_cost = _make_minimal_plan(cost_meta=None)


@then('the CLI dict should not have a "{key}" key')
def step_check_cli_dict_no_key(context: Context, key: str) -> None:
    assert key not in context.cli_dict, f"Key {key} unexpectedly in CLI dict"


@when('I add a budget exhaustion event of type "{btype}"')
def step_add_exhaustion_event(context: Context, btype: str) -> None:
    from cleveragents.domain.models.core.cost_metadata import BudgetExhaustionEvent

    event = BudgetExhaustionEvent(
        budget_type=btype,
        limit=10.0,
        used=11.0,
        provider="openai",
        model="gpt-4o",
    )
    context.cost_metadata.budget_exhaustion_events.append(event)


# ---- Additional coverage steps ----


@given(
    "I create a ProviderCostTable with custom entry for new provider "
    '"{provider}" model "{model}"'
)
def step_create_cost_table_new_provider(
    context: Context, provider: str, model: str
) -> None:
    from cleveragents.providers.cost_table import CostEntry, ProviderCostTable

    custom: dict[str, dict[str, CostEntry]] = {
        provider: {
            model: CostEntry(input_cost_per_token=0.005, output_cost_per_token=0.01)
        }
    }
    context.cost_table = ProviderCostTable(custom_entries=custom)


@then("listing models for empty provider raises ValueError")
def step_list_models_empty_provider(context: Context) -> None:
    try:
        context.cost_table.list_models("")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("estimating cost with negative input tokens raises ValueError on tracker")
def step_estimate_negative_input_tracker(context: Context) -> None:
    try:
        context.cost_tracker.estimate_cost("openai", "gpt-4o", -1, 0)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("estimating cost with negative output tokens raises ValueError on tracker")
def step_estimate_negative_output_tracker(context: Context) -> None:
    try:
        context.cost_tracker.estimate_cost("openai", "gpt-4o", 0, -1)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("recording tracked usage with negative output raises ValueError")
def step_record_tracked_negative_output(context: Context) -> None:
    try:
        context.cost_tracker.record_usage(
            context.cost_metadata,
            provider="openai",
            model="gpt-4o",
            input_tokens=0,
            output_tokens=-1,
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("the budget check should be warning")
def step_check_warning(context: Context) -> None:
    from cleveragents.providers.cost_tracker import BudgetStatus

    assert context.budget_result.status == BudgetStatus.WARNING, (
        f"Expected WARNING, got {context.budget_result.status}"
    )


@when('I get cost entry for "{provider}" "{model}"')
def step_get_cost_entry(context: Context, provider: str, model: str) -> None:
    context.returned_cost_entry = context.cost_tracker.get_cost_entry(provider, model)


@then("the returned cost entry should have positive input cost")
def step_check_returned_cost_entry(context: Context) -> None:
    assert context.returned_cost_entry.input_cost_per_token > 0


@then('the fallback skipped should mention "{reason}"')
def step_check_fallback_skipped_reason(context: Context, reason: str) -> None:
    reasons = [r for _, r in context.fallback_result.skipped]
    assert any(reason in r for r in reasons), (
        f"Expected '{reason}' in skipped reasons: {reasons}"
    )


@when("I select a fallback provider requiring streaming")
def step_select_fallback_streaming(context: Context) -> None:
    context.fallback_result = context.fallback_selector.select(require_streaming=True)


@when("I select a fallback provider requiring vision")
def step_select_fallback_vision(context: Context) -> None:
    context.fallback_result = context.fallback_selector.select(require_vision=True)


@when("I select a fallback provider requiring json mode")
def step_select_fallback_json_mode(context: Context) -> None:
    context.fallback_result = context.fallback_selector.select(require_json_mode=True)


@then("the fallback result should skip for missing streaming")
def step_check_skip_streaming(context: Context) -> None:
    reasons = [r for _, r in context.fallback_result.skipped]
    assert len(reasons) > 0, "Expected skipped entries"


@then("the fallback result should skip for missing vision")
def step_check_skip_vision(context: Context) -> None:
    reasons = [r for _, r in context.fallback_result.skipped]
    assert len(reasons) > 0, "Expected skipped entries"


@then("the fallback result should skip for missing json mode")
def step_check_skip_json_mode(context: Context) -> None:
    reasons = [r for _, r in context.fallback_result.skipped]
    assert len(reasons) > 0, "Expected skipped entries"
