"""Step definitions for the ACMS Hot Storage Tier feature."""

from __future__ import annotations

import threading
from typing import Any

from behave import given, then, when

from cleveragents.acms.storage.hot import HotStorageTier

__all__: list[str] = []


# ---------------------------------------------------------------------------
# Construction
# ---------------------------------------------------------------------------


@given("a HotStorageTier with no capacity limits")
def step_given_hot_tier_no_limits(context: Any) -> None:
    context.hot_tier = HotStorageTier()


@given("a HotStorageTier with max_entries {n:d}")
def step_given_hot_tier_max_entries(context: Any, n: int) -> None:
    context.hot_tier = HotStorageTier(max_entries=n)


@given("a HotStorageTier with max_bytes {n:d}")
def step_given_hot_tier_max_bytes(context: Any, n: int) -> None:
    context.hot_tier = HotStorageTier(max_bytes=n)


@given("a HotStorageTier with max_entries {entries:d} and max_bytes {bts:d}")
def step_given_hot_tier_both_limits(context: Any, entries: int, bts: int) -> None:
    context.hot_tier = HotStorageTier(max_entries=entries, max_bytes=bts)


@given("a HotStorageTier with max_entries {n:d} and an eviction recorder")
def step_given_hot_tier_with_recorder(context: Any, n: int) -> None:
    context.evicted_entries: list[tuple[str, str]] = []

    def recorder(entry_id: str, content: str) -> None:
        context.evicted_entries.append((entry_id, content))

    context.hot_tier = HotStorageTier(max_entries=n, on_evict=recorder)


@given("a HotStorageTier with max_entries {n:d} and a failing eviction callback")
def step_given_hot_tier_failing_callback(context: Any, n: int) -> None:
    def failing_callback(entry_id: str, content: str) -> None:
        raise RuntimeError("Simulated callback failure")

    context.hot_tier = HotStorageTier(max_entries=n, on_evict=failing_callback)


# ---------------------------------------------------------------------------
# Validation errors on construction
# ---------------------------------------------------------------------------


@then("creating a HotStorageTier with max_entries {n:d} should raise ValueError")
def step_then_max_entries_raises(context: Any, n: int) -> None:
    try:
        HotStorageTier(max_entries=n)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("creating a HotStorageTier with max_bytes {n:d} should raise ValueError")
def step_then_max_bytes_raises(context: Any, n: int) -> None:
    try:
        HotStorageTier(max_bytes=n)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Put / Get
# ---------------------------------------------------------------------------


@when('I put entry "{entry_id}" with content "{content}"')
def step_when_put_entry(context: Any, entry_id: str, content: str) -> None:
    context.hot_tier.put(entry_id, content)


@when('I get "{entry_id}" from the hot tier')
def step_when_get_entry(context: Any, entry_id: str) -> None:
    context.last_get_result = context.hot_tier.get(entry_id)


@then('getting "{entry_id}" from the hot tier should return "{expected}"')
def step_then_get_returns(context: Any, entry_id: str, expected: str) -> None:
    result = context.hot_tier.get(entry_id)
    assert result == expected, f"Expected {expected!r}, got {result!r}"


@then('getting "{entry_id}" from the hot tier should return None')
def step_then_get_returns_none(context: Any, entry_id: str) -> None:
    result = context.hot_tier.get(entry_id)
    assert result is None, f"Expected None, got {result!r}"


@then("putting an entry with empty id should raise ValueError")
def step_then_put_empty_id_raises(context: Any) -> None:
    try:
        context.hot_tier.put("", "content")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Remove
# ---------------------------------------------------------------------------


@when('I remove "{entry_id}" from the hot tier')
def step_when_remove_entry(context: Any, entry_id: str) -> None:
    context.last_remove_result = context.hot_tier.remove(entry_id)


@then('removing "{entry_id}" from the hot tier should return "{expected}"')
def step_then_remove_returns(context: Any, entry_id: str, expected: str) -> None:
    result = context.last_remove_result
    assert result == expected, f"Expected {expected!r}, got {result!r}"


@then('removing "{entry_id}" from the hot tier should return None')
def step_then_remove_returns_none(context: Any, entry_id: str) -> None:
    result = context.last_remove_result
    assert result is None, f"Expected None, got {result!r}"


# ---------------------------------------------------------------------------
# Clear
# ---------------------------------------------------------------------------


@when("I clear the hot tier")
def step_when_clear(context: Any) -> None:
    context.hot_tier.clear()


# ---------------------------------------------------------------------------
# Metrics assertions
# ---------------------------------------------------------------------------


@then("the hot tier entry_count should be {n:d}")
def step_then_entry_count(context: Any, n: int) -> None:
    assert context.hot_tier.entry_count == n, (
        f"Expected entry_count={n}, got {context.hot_tier.entry_count}"
    )


@then("the hot storage tier size_bytes should be {n:d}")
def step_then_size_bytes(context: Any, n: int) -> None:
    assert context.hot_tier.size_bytes == n, (
        f"Expected size_bytes={n}, got {context.hot_tier.size_bytes}"
    )


@then("the hot storage tier size_bytes should be at most {n:d}")
def step_then_size_bytes_at_most(context: Any, n: int) -> None:
    assert context.hot_tier.size_bytes <= n, (
        f"Expected size_bytes <= {n}, got {context.hot_tier.size_bytes}"
    )


@then("the hot tier hit_count should be {n:d}")
def step_then_hit_count(context: Any, n: int) -> None:
    assert context.hot_tier.hit_count == n, (
        f"Expected hit_count={n}, got {context.hot_tier.hit_count}"
    )


@then("the hot tier miss_count should be {n:d}")
def step_then_miss_count(context: Any, n: int) -> None:
    assert context.hot_tier.miss_count == n, (
        f"Expected miss_count={n}, got {context.hot_tier.miss_count}"
    )


@then("the hot tier max_entries should be {n:d}")
def step_then_max_entries(context: Any, n: int) -> None:
    assert context.hot_tier.max_entries == n, (
        f"Expected max_entries={n}, got {context.hot_tier.max_entries}"
    )


@then("the hot tier max_entries should be None")
def step_then_max_entries_none(context: Any) -> None:
    assert context.hot_tier.max_entries is None, (
        f"Expected max_entries=None, got {context.hot_tier.max_entries}"
    )


@then("the hot tier max_bytes should be {n:d}")
def step_then_max_bytes(context: Any, n: int) -> None:
    assert context.hot_tier.max_bytes == n, (
        f"Expected max_bytes={n}, got {context.hot_tier.max_bytes}"
    )


@then("the hot tier max_bytes should be None")
def step_then_max_bytes_none(context: Any) -> None:
    assert context.hot_tier.max_bytes is None, (
        f"Expected max_bytes=None, got {context.hot_tier.max_bytes}"
    )


@then("the hot tier entry_count should be at most {n:d}")
def step_then_entry_count_at_most(context: Any, n: int) -> None:
    assert context.hot_tier.entry_count <= n, (
        f"Expected entry_count <= {n}, got {context.hot_tier.entry_count}"
    )


# ---------------------------------------------------------------------------
# Eviction callback assertions
# ---------------------------------------------------------------------------


@then('the eviction recorder should have recorded entry id "{entry_id}"')
def step_then_eviction_recorded_id(context: Any, entry_id: str) -> None:
    recorded_ids = [e[0] for e in context.evicted_entries]
    assert entry_id in recorded_ids, (
        f"Expected {entry_id!r} in evicted entries, got {recorded_ids}"
    )


@then(
    'the eviction recorder should have recorded entry "{entry_id}" '
    'with content "{content}"'
)
def step_then_eviction_recorded_with_content(
    context: Any, entry_id: str, content: str
) -> None:
    for eid, econtent in context.evicted_entries:
        if eid == entry_id:
            assert econtent == content, (
                f"Expected content {content!r} for evicted {entry_id!r}, "
                f"got {econtent!r}"
            )
            return
    raise AssertionError(
        f"Entry {entry_id!r} not found in evicted entries: {context.evicted_entries}"
    )


@then("the eviction recorder should have recorded {n:d} evictions")
def step_then_eviction_count(context: Any, n: int) -> None:
    assert len(context.evicted_entries) == n, (
        f"Expected {n} evictions, got {len(context.evicted_entries)}: "
        f"{context.evicted_entries}"
    )


# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------


@when(
    "{num_threads:d} threads concurrently put {entries_per_thread:d} entries each "
    "into the hot tier"
)
def step_when_concurrent_puts(
    context: Any, num_threads: int, entries_per_thread: int
) -> None:
    context.concurrent_exception: Exception | None = None
    barrier = threading.Barrier(num_threads)

    def worker(thread_idx: int) -> None:
        try:
            barrier.wait()
            for i in range(entries_per_thread):
                key = f"thread-{thread_idx}-entry-{i}"
                context.hot_tier.put(key, f"content-{thread_idx}-{i}")
        except Exception as exc:
            context.concurrent_exception = exc

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(num_threads)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()


@when(
    "{num_threads:d} threads concurrently put entries and "
    "{num_threads2:d} threads concurrently get entries"
)
def step_when_concurrent_puts_and_gets(
    context: Any, num_threads: int, num_threads2: int
) -> None:
    context.concurrent_exception = None
    total_threads = num_threads + num_threads2
    barrier = threading.Barrier(total_threads)

    # Pre-populate some entries for getters to find
    for i in range(20):
        context.hot_tier.put(f"pre-{i}", f"pre-content-{i}")

    def putter(thread_idx: int) -> None:
        try:
            barrier.wait()
            for i in range(10):
                context.hot_tier.put(f"put-{thread_idx}-{i}", f"val-{i}")
        except Exception as exc:
            context.concurrent_exception = exc

    def getter(thread_idx: int) -> None:
        try:
            barrier.wait()
            for i in range(10):
                context.hot_tier.get(f"pre-{i % 20}")
        except Exception as exc:
            context.concurrent_exception = exc

    threads = [
        threading.Thread(target=putter, args=(i,)) for i in range(num_threads)
    ] + [threading.Thread(target=getter, args=(i,)) for i in range(num_threads2)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()


@when("{num_threads:d} threads concurrently put large entries into the hot tier")
def step_when_concurrent_large_puts(context: Any, num_threads: int) -> None:
    context.concurrent_exception = None
    barrier = threading.Barrier(num_threads)

    def worker(thread_idx: int) -> None:
        try:
            barrier.wait()
            for i in range(5):
                key = f"large-{thread_idx}-{i}"
                # Each entry is ~20 bytes, well above max_bytes=100 per thread
                context.hot_tier.put(key, f"content-{thread_idx:03d}-{i:03d}-pad")
        except Exception as exc:
            context.concurrent_exception = exc

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(num_threads)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()


@then("no exception should have been raised during concurrent puts")
def step_then_no_concurrent_put_exception(context: Any) -> None:
    assert context.concurrent_exception is None, (
        f"Concurrent puts raised: {context.concurrent_exception}"
    )


@then("no exception should have been raised during concurrent gets and puts")
def step_then_no_concurrent_get_put_exception(context: Any) -> None:
    assert context.concurrent_exception is None, (
        f"Concurrent gets/puts raised: {context.concurrent_exception}"
    )


@then("no exception should have been raised during concurrent evictions")
def step_then_no_concurrent_eviction_exception(context: Any) -> None:
    assert context.concurrent_exception is None, (
        f"Concurrent evictions raised: {context.concurrent_exception}"
    )
