"""Integration tests for ImmuKV client using MinIO.

These tests require MinIO running and test actual S3 operations.
Run with: IMMUKV_INTEGRATION_TEST=true IMMUKV_S3_ENDPOINT=http://localhost:9000 pytest
"""

import asyncio
import concurrent.futures
import os
import uuid
from collections.abc import Coroutine
from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Generator, TypeVar, cast

import pytest

if TYPE_CHECKING:
    from types_aiobotocore_s3.client import S3Client

from immukv import Config, ImmuKVClient
from immukv._internal.types import RawEntry
from immukv.json_helpers import JSONValue
from immukv.types import S3Credentials, S3Overrides

# Skip if not in integration test mode
pytestmark = pytest.mark.skipif(
    os.getenv("IMMUKV_INTEGRATION_TEST") != "true",
    reason="Integration tests require IMMUKV_INTEGRATION_TEST=true",
)


def identity_decoder(value: JSONValue) -> object:
    """Identity decoder that returns the JSONValue as-is."""
    return value


def identity_encoder(value: object) -> JSONValue:
    """Identity encoder that returns the value as JSONValue."""
    return value  # type: ignore[return-value]


@pytest.fixture(scope="session")
def _aio_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
    """Provide a background event loop for test fixtures."""
    import threading

    loop = asyncio.new_event_loop()
    thread = threading.Thread(target=loop.run_forever, name="test-io", daemon=True)
    thread.start()
    yield loop
    loop.call_soon_threadsafe(loop.stop)
    thread.join(timeout=5)
    loop.close()


@pytest.fixture(scope="session")
def raw_s3(_aio_loop: asyncio.AbstractEventLoop) -> Generator["S3Client", None, None]:
    """Create raw aiobotocore S3 client for bucket management operations."""
    import aiobotocore.session

    endpoint_url = os.getenv("IMMUKV_S3_ENDPOINT", "http://localhost:4566")
    access_key = os.getenv("AWS_ACCESS_KEY_ID", "test")
    secret_key = os.getenv("AWS_SECRET_ACCESS_KEY", "test")

    async def _create() -> tuple["S3Client", AsyncExitStack]:
        stack = AsyncExitStack()
        session = aiobotocore.session.get_session()
        client: "S3Client" = await stack.enter_async_context(
            session.create_client(
                "s3",
                endpoint_url=endpoint_url,
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
                region_name="us-east-1",
            )
        )
        return client, stack

    future = asyncio.run_coroutine_threadsafe(_create(), _aio_loop)
    client, stack = future.result()
    yield client
    future_close = asyncio.run_coroutine_threadsafe(stack.aclose(), _aio_loop)
    future_close.result()


_T = TypeVar("_T")


def _run_sync(coro: Coroutine[object, object, _T], loop: asyncio.AbstractEventLoop) -> _T:
    """Run a coroutine on the background loop, blocking until complete."""
    future: concurrent.futures.Future[_T] = asyncio.run_coroutine_threadsafe(
        coro,
        loop,
    )
    return future.result()


@pytest.fixture  # type: ignore[misc]
def s3_bucket(
    raw_s3: "S3Client", _aio_loop: asyncio.AbstractEventLoop
) -> Generator[str, None, None]:
    """Create unique S3 bucket for each test - ensures complete isolation."""
    bucket_name = f"test-immukv-{uuid.uuid4().hex[:8]}"

    # Create bucket
    _run_sync(raw_s3.create_bucket(Bucket=bucket_name), _aio_loop)

    # Enable versioning
    _run_sync(
        raw_s3.put_bucket_versioning(
            Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
        ),
        _aio_loop,
    )

    yield bucket_name

    # Cleanup: Delete all versions, delete markers, then bucket
    try:
        response = _run_sync(raw_s3.list_object_versions(Bucket=bucket_name), _aio_loop)

        # Delete all versions
        for version in response.get("Versions", []):  # type: ignore[misc,attr-defined]
            _run_sync(
                raw_s3.delete_object(
                    Bucket=bucket_name,
                    Key=version["Key"],  # type: ignore[misc]
                    VersionId=version["VersionId"],  # type: ignore[misc]
                ),
                _aio_loop,
            )

        # Delete all delete markers
        for marker in response.get("DeleteMarkers", []):  # type: ignore[misc,attr-defined]
            _run_sync(
                raw_s3.delete_object(
                    Bucket=bucket_name,
                    Key=marker["Key"],  # type: ignore[misc]
                    VersionId=marker["VersionId"],  # type: ignore[misc]
                ),
                _aio_loop,
            )

        # Delete bucket
        _run_sync(raw_s3.delete_bucket(Bucket=bucket_name), _aio_loop)
    except Exception as e:
        # Best effort cleanup - don't fail tests if cleanup fails
        print(f"Warning: Cleanup failed for bucket {bucket_name}: {e}")


@pytest.fixture  # type: ignore[misc]
def client(s3_bucket: str) -> Generator[ImmuKVClient[str, object], None, None]:
    """Create ImmuKV client connected to MinIO."""
    endpoint_url = os.getenv("IMMUKV_S3_ENDPOINT", "http://localhost:9000")
    access_key = os.getenv("AWS_ACCESS_KEY_ID", "test")
    secret_key = os.getenv("AWS_SECRET_ACCESS_KEY", "test")

    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test/",
        repair_check_interval_ms=1000,
        overrides=S3Overrides(
            endpoint_url=endpoint_url,
            credentials=S3Credentials(
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
            ),
            force_path_style=True,
        ),
    )

    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )
    with client_instance as client:
        yield client


def test_set_and_get_single_entry(client: ImmuKVClient[str, object]) -> None:
    """Test basic set and get operations."""
    # Set a value
    entry = client.set("key1", {"data": "value1"})

    # Verify entry structure
    assert entry.key == "key1"
    assert entry.value == {"data": "value1"}
    assert entry.sequence == 0
    assert entry.previous_hash == "sha256:genesis"
    assert entry.hash.startswith("sha256:")
    assert entry.version_id is not None
    assert entry.previous_version_id is None

    # Get the value back
    retrieved = client.get("key1")

    assert retrieved.key == entry.key
    assert retrieved.value == entry.value
    assert retrieved.sequence == entry.sequence
    assert retrieved.hash == entry.hash


def test_set_multiple_entries_same_key(client: ImmuKVClient[str, object]) -> None:
    """Test multiple writes to the same key."""
    # Write three values to the same key
    entry1 = client.set("sensor-01", {"temp": 20.5})
    entry2 = client.set("sensor-01", {"temp": 21.0})
    entry3 = client.set("sensor-01", {"temp": 19.8})

    # Verify sequence numbers are contiguous
    assert entry1.sequence == 0
    assert entry2.sequence == 1
    assert entry3.sequence == 2

    # Verify hash chain
    assert entry1.previous_hash == "sha256:genesis"
    assert entry2.previous_hash == entry1.hash
    assert entry3.previous_hash == entry2.hash

    # Get should return latest value
    latest = client.get("sensor-01")
    assert latest.value == {"temp": 19.8}
    assert latest.sequence == 2


def test_set_multiple_keys(client: ImmuKVClient[str, object]) -> None:
    """Test writing to multiple different keys."""
    entry1 = client.set("key-a", {"value": "a"})
    entry2 = client.set("key-b", {"value": "b"})
    entry3 = client.set("key-c", {"value": "c"})

    # All entries should be in the log with contiguous sequences
    assert entry1.sequence == 0
    assert entry2.sequence == 1
    assert entry3.sequence == 2

    # Each key should retrieve its own value
    assert client.get("key-a").value == {"value": "a"}
    assert client.get("key-b").value == {"value": "b"}
    assert client.get("key-c").value == {"value": "c"}


def test_history_single_key(client: ImmuKVClient[str, object]) -> None:
    """Test retrieving history for a single key."""
    # Write multiple versions
    client.set("metric", {"count": 1})
    client.set("metric", {"count": 2})
    client.set("metric", {"count": 3})

    # Get full history
    entries, oldest_version = client.history("metric", None, None)

    # Should return all 3 entries in descending order (newest first)
    assert len(entries) == 3
    assert entries[0].value == {"count": 3}
    assert entries[1].value == {"count": 2}
    assert entries[2].value == {"count": 1}

    # Verify sequences
    assert entries[0].sequence == 2
    assert entries[1].sequence == 1
    assert entries[2].sequence == 0


def test_history_with_limit(client: ImmuKVClient[str, object]) -> None:
    """Test history retrieval with limit."""
    # Write 5 versions
    for i in range(5):
        client.set("counter", {"value": i})

    # Get only first 3
    entries, oldest_version = client.history("counter", None, 3)

    assert len(entries) == 3
    assert entries[0].value == {"value": 4}  # Newest
    assert entries[1].value == {"value": 3}
    assert entries[2].value == {"value": 2}


def test_history_mixed_keys(client: ImmuKVClient[str, object]) -> None:
    """Test that history only returns entries for requested key."""
    # Mix writes to different keys
    client.set("key-x", {"data": "x1"})
    client.set("key-y", {"data": "y1"})
    client.set("key-x", {"data": "x2"})
    client.set("key-y", {"data": "y2"})
    client.set("key-x", {"data": "x3"})

    # Get history for key-x
    entries, _ = client.history("key-x", None, None)

    # Should only have 3 entries for key-x
    assert len(entries) == 3
    assert all(e.key == "key-x" for e in entries)
    assert entries[0].value == {"data": "x3"}
    assert entries[1].value == {"data": "x2"}
    assert entries[2].value == {"data": "x1"}


def test_log_entries(client: ImmuKVClient[str, object]) -> None:
    """Test retrieving entries from global log."""
    # Write to multiple keys
    client.set("k1", {"v": 1})
    client.set("k2", {"v": 2})
    client.set("k1", {"v": 3})

    # Get all log entries
    entries = client.log_entries(None, None)

    # Should have 3 entries in descending order (newest first)
    assert len(entries) == 3
    assert entries[0].key == "k1"
    assert entries[0].value == {"v": 3}
    assert entries[0].sequence == 2

    assert entries[1].key == "k2"
    assert entries[1].value == {"v": 2}
    assert entries[1].sequence == 1

    assert entries[2].key == "k1"
    assert entries[2].value == {"v": 1}
    assert entries[2].sequence == 0


def test_log_entries_with_limit(client: ImmuKVClient[str, object]) -> None:
    """Test log retrieval with limit."""
    # Write 5 entries
    for i in range(5):
        client.set(f"key-{i}", {"index": i})

    # Get only 3 newest
    entries = client.log_entries(None, 3)

    assert len(entries) == 3
    assert entries[0].sequence == 4
    assert entries[1].sequence == 3
    assert entries[2].sequence == 2


def test_list_keys(client: ImmuKVClient[str, object]) -> None:
    """Test listing all keys."""
    # Write to multiple keys (not in alphabetical order)
    client.set("zebra", {"animal": "z"})
    client.set("apple", {"fruit": "a"})
    client.set("banana", {"fruit": "b"})

    # List all keys
    keys = client.list_keys(None, None)

    # Should return in lexicographic order
    assert len(keys) == 3
    assert keys == ["apple", "banana", "zebra"]


def test_list_keys_with_pagination(client: ImmuKVClient[str, object]) -> None:
    """Test key listing with pagination."""
    # Create several keys
    for i in range(5):
        client.set(f"key-{i:02d}", {"index": i})

    # Get first 3 keys
    keys = client.list_keys(None, 3)
    assert len(keys) == 3
    assert keys == ["key-00", "key-01", "key-02"]

    # Get next batch after "key-01"
    keys = client.list_keys("key-01", 2)
    assert len(keys) == 2
    assert keys == ["key-02", "key-03"]


def test_list_keys_with_prefix(client: ImmuKVClient[str, object]) -> None:
    """Test listing keys filtered by prefix."""
    client.set("users/alice", {"role": "admin"})
    client.set("users/bob", {"role": "user"})
    client.set("orders/001", {"total": 100})
    client.set("orders/002", {"total": 200})
    client.set("config", {"debug": False})

    # Filter by "users/" prefix
    user_keys = client.list_keys_with_prefix("users/", None, None)
    assert user_keys == ["users/alice", "users/bob"]

    # Filter by "orders/" prefix
    order_keys = client.list_keys_with_prefix("orders/", None, None)
    assert order_keys == ["orders/001", "orders/002"]

    # Without prefix should return all keys
    all_keys = client.list_keys(None, None)
    assert len(all_keys) == 5


def test_list_keys_with_prefix_and_limit(client: ImmuKVClient[str, object]) -> None:
    """Test listing keys with both prefix and limit."""
    client.set("items/a", {"v": 1})
    client.set("items/b", {"v": 2})
    client.set("items/c", {"v": 3})
    client.set("other/x", {"v": 4})

    keys = client.list_keys_with_prefix("items/", None, 2)
    assert len(keys) == 2
    assert keys == ["items/a", "items/b"]


def test_list_keys_with_prefix_no_matches(client: ImmuKVClient[str, object]) -> None:
    """Test listing keys with prefix that matches nothing."""
    client.set("users/alice", {"role": "admin"})

    keys = client.list_keys_with_prefix("nonexistent/", None, None)
    assert keys == []


def test_verify_single_entry(client: ImmuKVClient[str, object]) -> None:
    """Test verifying a single entry's hash."""
    entry = client.set("test-key", {"field": "value"})

    # Verify should pass for valid entry
    assert client.verify(entry) is True


def test_verify_corrupted_entry(client: ImmuKVClient[str, object]) -> None:
    """Test that verification fails for corrupted entry."""
    entry = client.set("test-key", {"field": "value"})

    # Corrupt the entry
    entry.value["field"] = "corrupted"  # type: ignore[index]

    # Verification should fail
    assert client.verify(entry) is False


def test_verify_log_chain(client: ImmuKVClient[str, object]) -> None:
    """Test verifying the entire log hash chain."""
    # Write several entries
    client.set("k1", {"v": 1})
    client.set("k2", {"v": 2})
    client.set("k3", {"v": 3})

    # Verify entire chain
    assert client.verify_log_chain() is True


def test_verify_log_chain_with_limit(client: ImmuKVClient[str, object]) -> None:
    """Test verifying only recent entries in chain."""
    # Write several entries
    for i in range(10):
        client.set(f"key-{i}", {"index": i})

    # Verify only last 5 entries
    assert client.verify_log_chain(limit=5) is True


def test_get_nonexistent_key(client: ImmuKVClient[str, object]) -> None:
    """Test getting a key that doesn't exist."""
    from immukv.client import KeyNotFoundError

    # Write one key
    client.set("existing-key", {"data": "value"})

    # Try to get non-existent key
    with pytest.raises(KeyNotFoundError):
        client.get("nonexistent-key")


def test_history_nonexistent_key(client: ImmuKVClient[str, object]) -> None:
    """Test history for a key that was never written."""
    # Write to other keys
    client.set("other-key", {"data": "value"})

    # History for non-existent key should return empty list
    entries, oldest_version = client.history("nonexistent-key", None, None)
    assert entries == []
    assert oldest_version is None


def test_hash_chain_integrity(client: ImmuKVClient[str, object]) -> None:
    """Test that hash chain links entries correctly."""
    entry1 = client.set("chain-test", {"step": 1})
    entry2 = client.set("chain-test", {"step": 2})
    entry3 = client.set("chain-test", {"step": 3})

    # Verify chain links
    assert entry1.previous_hash == "sha256:genesis"
    assert entry2.previous_hash == entry1.hash
    assert entry3.previous_hash == entry2.hash

    # Each hash should be unique
    assert entry1.hash != entry2.hash
    assert entry2.hash != entry3.hash
    assert entry1.hash != entry3.hash


def test_sequence_numbers_contiguous(client: ImmuKVClient[str, object]) -> None:
    """Test that sequence numbers are contiguous across different keys."""
    entries = []

    # Mix writes to different keys
    entries.append(client.set("a", {"v": 1}))
    entries.append(client.set("b", {"v": 2}))
    entries.append(client.set("a", {"v": 3}))
    entries.append(client.set("c", {"v": 4}))
    entries.append(client.set("b", {"v": 5}))

    # Verify all sequences are contiguous
    for i, entry in enumerate(entries):
        assert entry.sequence == i


def test_get_log_version(client: ImmuKVClient[str, object]) -> None:
    """Test retrieving a specific log entry by version id."""
    entry1 = client.set("versioned", {"data": "first"})
    entry2 = client.set("versioned", {"data": "second"})

    # Retrieve first entry by its version id
    retrieved = client.get_log_version(entry1.version_id)

    assert retrieved.sequence == entry1.sequence
    assert retrieved.value == {"data": "first"}
    assert retrieved.hash == entry1.hash


def test_read_only_mode(client: ImmuKVClient[str, object]) -> None:
    """Test that read-only mode works correctly."""
    # First write some data with normal client
    client.set("readonly-test", {"value": "data"})

    # Create a read-only client
    config = Config(
        s3_bucket=client._config.s3_bucket,
        s3_region=client._config.s3_region,
        s3_prefix=client._config.s3_prefix,
        read_only=True,
        overrides=client._config.overrides,
    )

    ro_client: ImmuKVClient[str, object] = ImmuKVClient(config, identity_decoder, identity_encoder)
    with ro_client:
        # Read should work
        entry = ro_client.get("readonly-test")
        assert entry.value == {"value": "data"}

        # Write should fail (implementation detail - may raise or handle differently)
        # For now, just verify read works


def test_custom_endpoint_url_config(s3_bucket: str) -> None:
    """Test that overrides can be specified for S3-compatible services."""
    # Config with overrides should be accepted
    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test/",
        overrides=S3Overrides(endpoint_url="http://localhost:4566"),
    )

    # Client creation should succeed
    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )

    # Verify overrides are stored in config
    assert client_instance._config.overrides is not None
    assert client_instance._config.overrides.endpoint_url == "http://localhost:4566"

    # Note: Actual operations would require MinIO/moto running at that endpoint.
    # This test verifies the config accepts and stores the overrides correctly.


def test_default_overrides_is_none(s3_bucket: str) -> None:
    """Test that overrides defaults to None for AWS S3."""
    # Config without overrides specified
    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test/",
    )

    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )

    # Should default to None (uses AWS S3)
    assert client_instance._config.overrides is None


def test_orphan_status_false_does_not_return_orphan(client: ImmuKVClient[str, object]) -> None:
    """Test that is_orphaned=False does not trigger orphan fallback in get()."""
    from immukv.types import KeyNotFoundError

    # Create an entry and set it as orphan status with is_orphaned=False
    entry = client.set("test-key", {"value": "orphan_data"})

    # Set read-only mode so orphan fallback would be checked
    client._can_write = False

    raw_entry = RawEntry(
        key=entry.key,
        value=cast(JSONValue, entry.value),
        timestamp_ms=entry.timestamp_ms,
        version_id=entry.version_id,
        sequence=entry.sequence,
        previous_version_id=entry.previous_version_id,
        hash=entry.hash,
        previous_hash=entry.previous_hash,
        previous_key_object_etag=entry.previous_key_object_etag,
    )

    client._latest_orphan_status = {
        "is_orphaned": False,  # Explicitly False - repair completed
        "orphan_key": "nonexistent-key",
        "orphan_entry": raw_entry,
        "checked_at": 0,
    }

    # get() should raise KeyNotFoundError, NOT return the orphan entry
    # Even though all other conditions match (read-only, key matches, entry exists)
    # is_orphaned=False should prevent the orphan fallback
    with pytest.raises(KeyNotFoundError, match="Key 'nonexistent-key' not found"):
        client.get("nonexistent-key")


def test_orphan_status_true_returns_orphan_in_readonly(
    client: ImmuKVClient[str, object],
) -> None:
    """Test that is_orphaned=True correctly returns orphan entry in read-only mode."""
    # Create an entry
    entry = client.set("existing-key", {"value": "test_data"})

    # Set read-only mode
    client._can_write = False

    # Simulate orphan status with is_orphaned=True for a nonexistent key
    raw_entry = RawEntry(
        key=entry.key,
        value=cast(JSONValue, entry.value),
        timestamp_ms=entry.timestamp_ms,
        version_id=entry.version_id,
        sequence=entry.sequence,
        previous_version_id=entry.previous_version_id,
        hash=entry.hash,
        previous_hash=entry.previous_hash,
        previous_key_object_etag=entry.previous_key_object_etag,
    )

    client._latest_orphan_status = {
        "is_orphaned": True,  # Explicitly True - orphan exists
        "orphan_key": "orphaned-key",
        "orphan_entry": raw_entry,
        "checked_at": 0,
    }

    # get() on the orphaned key should return the orphan entry (not raise)
    # Even though the key object doesn't exist in S3
    result = client.get("orphaned-key")

    # Should return the cached orphan entry
    assert result.value == {"value": "test_data"}
    assert result.key == "existing-key"  # Original key from entry


def test_orphan_status_false_does_not_prepend_in_history(
    client: ImmuKVClient[str, object],
) -> None:
    """Test that is_orphaned=False does not prepend orphan entry in history()."""
    # Create a key with history
    client.set("test-key", {"value": "v1"})
    entry2 = client.set("test-key", {"value": "v2"})

    # Set orphan status with is_orphaned=False
    raw_entry2 = RawEntry(
        key=entry2.key,
        value=cast(JSONValue, entry2.value),
        timestamp_ms=entry2.timestamp_ms,
        version_id=entry2.version_id,
        sequence=entry2.sequence,
        previous_version_id=entry2.previous_version_id,
        hash=entry2.hash,
        previous_hash=entry2.previous_hash,
        previous_key_object_etag=entry2.previous_key_object_etag,
    )

    client._latest_orphan_status = {
        "is_orphaned": False,  # Explicitly False
        "orphan_key": "test-key",
        "orphan_entry": raw_entry2,
        "checked_at": 0,
    }

    # Get history - should NOT include orphan entry as first item
    entries, _ = client.history("test-key", None, None)

    # Should have 2 entries (v2 and v1), NOT 3 (orphan + v2 + v1)
    assert len(entries) == 2
    assert entries[0].value == {"value": "v2"}
    assert entries[1].value == {"value": "v1"}


def test_orphan_status_true_prepends_in_history(client: ImmuKVClient[str, object]) -> None:
    """Test that is_orphaned=True correctly prepends orphan entry in history()."""
    # Create a key with history
    client.set("test-key", {"value": "v1"})
    entry2 = client.set("test-key", {"value": "v2"})

    # Set orphan status with is_orphaned=True
    raw_entry2 = RawEntry(
        key=entry2.key,
        value=cast(JSONValue, entry2.value),
        timestamp_ms=entry2.timestamp_ms,
        version_id=entry2.version_id,
        sequence=entry2.sequence,
        previous_version_id=entry2.previous_version_id,
        hash=entry2.hash,
        previous_hash=entry2.previous_hash,
        previous_key_object_etag=entry2.previous_key_object_etag,
    )

    client._latest_orphan_status = {
        "is_orphaned": True,  # Explicitly True
        "orphan_key": "test-key",
        "orphan_entry": raw_entry2,
        "checked_at": 0,
    }

    # Get history - should include orphan entry as first item
    entries, _ = client.history("test-key", None, None)

    # Should have 3 entries: orphan (v2) + v2 + v1
    # Note: This creates a duplicate entry, which is the expected behavior
    # when orphan repair hasn't completed yet
    assert len(entries) == 3
    assert entries[0].value == {"value": "v2"}  # Orphan entry
    assert entries[1].value == {"value": "v2"}  # Actual latest
    assert entries[2].value == {"value": "v1"}


def test_orphan_status_none_does_not_trigger_orphan_fallback(
    client: ImmuKVClient[str, object],
) -> None:
    """Test that is_orphaned=None (undefined) does not trigger orphan fallback in get() or history().

    When is_orphaned has not been determined yet (None/undefined), the behavior should
    match is_orphaned=False: no fallback in get(), no prepend in history().
    """
    from immukv.types import KeyNotFoundError

    # Create an entry
    entry = client.set("test-key", {"value": "test_data"})

    # Set orphan status with is_orphaned not present (simulating undefined/None)
    raw_entry = RawEntry(
        key=entry.key,
        value=cast(JSONValue, entry.value),
        timestamp_ms=entry.timestamp_ms,
        version_id=entry.version_id,
        sequence=entry.sequence,
        previous_version_id=entry.previous_version_id,
        hash=entry.hash,
        previous_hash=entry.previous_hash,
        previous_key_object_etag=entry.previous_key_object_etag,
    )

    # Intentionally malformed OrphanStatus: omit is_orphaned and checked_at
    # to simulate undefined/missing state. Uses type: ignore to bypass
    # TypedDict requirements (matches TypeScript's `as any` pattern).
    client._latest_orphan_status = {  # type: ignore[assignment]
        "orphan_key": "nonexistent-key",
        "orphan_entry": raw_entry,
    }

    # get() on nonexistent key should raise KeyNotFoundError (no orphan fallback)
    with pytest.raises(KeyNotFoundError, match="Key 'nonexistent-key' not found"):
        client.get("nonexistent-key")

    # Now test history() behavior with is_orphaned=None
    # Write a second entry to test-key so history has content
    entry2 = client.set("test-key", {"value": "v2"})

    raw_entry2 = RawEntry(
        key=entry2.key,
        value=cast(JSONValue, entry2.value),
        timestamp_ms=entry2.timestamp_ms,
        version_id=entry2.version_id,
        sequence=entry2.sequence,
        previous_version_id=entry2.previous_version_id,
        hash=entry2.hash,
        previous_hash=entry2.previous_hash,
        previous_key_object_etag=entry2.previous_key_object_etag,
    )

    client._latest_orphan_status = {  # type: ignore[assignment]
        "orphan_key": "test-key",
        "orphan_entry": raw_entry2,
    }

    # Get history - should NOT include orphan entry as first item
    entries, _ = client.history("test-key", None, None)

    # Should have 2 entries (v2 and test_data), NOT 3 (orphan + v2 + test_data)
    assert len(entries) == 2
    assert entries[0].value == {"value": "v2"}
    assert entries[1].value == {"value": "test_data"}


def test_with_codec_creates_client_with_different_codec(
    client: ImmuKVClient[str, object],
) -> None:
    """Test that with_codec creates a new client with different codec sharing S3 connection."""
    from typing import TypedDict, cast

    # Write with original client
    client.set("shared-key", {"original": True})

    # Create derived client with different codec
    class CustomValue(TypedDict):
        transformed: bool

    def custom_decoder(json: JSONValue) -> CustomValue:
        return {"transformed": cast(dict[str, object], json).get("original") is True}

    def custom_encoder(value: CustomValue) -> JSONValue:
        return {"original": value["transformed"]}

    derived_client: ImmuKVClient[str, CustomValue] = client.with_codec(
        custom_decoder, custom_encoder
    )

    # Read with derived client - should decode with custom decoder
    entry = derived_client.get("shared-key")
    assert entry.value == {"transformed": True}

    # Write with derived client
    derived_client.set("custom-key", {"transformed": False})

    # Read back with original client - should see the encoded value
    original_entry = client.get("custom-key")
    assert original_entry.value == {"original": False}


def test_with_codec_shares_s3_connection(client: ImmuKVClient[str, object]) -> None:
    """Test that derived client shares S3 connection with original."""
    derived_client: ImmuKVClient[str, object] = client.with_codec(
        identity_decoder, identity_encoder
    )

    # Both should reference the same S3 client instance
    assert derived_client._s3 is client._s3
    assert derived_client._config is client._config


def test_with_codec_has_independent_mutable_state(client: ImmuKVClient[str, object]) -> None:
    """Test that derived client has independent mutable state."""
    derived_client: ImmuKVClient[str, object] = client.with_codec(
        identity_decoder, identity_encoder
    )

    # Mutable state should be independent
    assert derived_client._last_repair_check_ms == 0
    assert derived_client._can_write is None
    assert derived_client._latest_orphan_status is None

    # Modify original client's state
    client._last_repair_check_ms = 12345
    client._can_write = True

    # Derived client should be unaffected
    assert derived_client._last_repair_check_ms == 0
    assert derived_client._can_write is None


# --- Credential Integration Tests ---


def test_session_token_passthrough(s3_bucket: str) -> None:
    """Test that a session token is passed through without breaking the client."""
    endpoint_url = os.getenv("IMMUKV_S3_ENDPOINT", "http://localhost:9000")
    access_key = os.getenv("AWS_ACCESS_KEY_ID", "test")
    secret_key = os.getenv("AWS_SECRET_ACCESS_KEY", "test")

    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test-session-token/",
        repair_check_interval_ms=1000,
        overrides=S3Overrides(
            endpoint_url=endpoint_url,
            credentials=S3Credentials(
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
                aws_session_token=None,
            ),
            force_path_style=True,
        ),
    )

    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )
    with client_instance as c:
        # Basic set/get should work with session token present
        entry = c.set("session-key", {"data": "session-value"})
        assert entry.key == "session-key"
        assert entry.value == {"data": "session-value"}

        retrieved = c.get("session-key")
        assert retrieved.value == {"data": "session-value"}


def test_credential_provider(s3_bucket: str) -> None:
    """Test that an async credential provider function works for authentication."""
    endpoint_url = os.getenv("IMMUKV_S3_ENDPOINT", "http://localhost:9000")
    access_key = os.getenv("AWS_ACCESS_KEY_ID", "test")
    secret_key = os.getenv("AWS_SECRET_ACCESS_KEY", "test")

    async def provide_credentials() -> S3Credentials:
        return S3Credentials(
            aws_access_key_id=access_key,
            aws_secret_access_key=secret_key,
        )

    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test-cred-provider/",
        repair_check_interval_ms=1000,
        overrides=S3Overrides(
            endpoint_url=endpoint_url,
            credentials=provide_credentials,
            force_path_style=True,
        ),
    )

    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )
    with client_instance as c:
        entry = c.set("provider-key", {"data": "provider-value"})
        assert entry.key == "provider-key"
        assert entry.value == {"data": "provider-value"}

        retrieved = c.get("provider-key")
        assert retrieved.value == {"data": "provider-value"}


def test_credential_provider_with_refresh(s3_bucket: str) -> None:
    """Test that the credential provider is called (AioDeferredRefreshableCredentials wiring)."""
    endpoint_url = os.getenv("IMMUKV_S3_ENDPOINT", "http://localhost:9000")
    access_key = os.getenv("AWS_ACCESS_KEY_ID", "test")
    secret_key = os.getenv("AWS_SECRET_ACCESS_KEY", "test")

    call_count = 0

    async def counting_provider() -> S3Credentials:
        nonlocal call_count
        call_count += 1
        return S3Credentials(
            aws_access_key_id=access_key,
            aws_secret_access_key=secret_key,
        )

    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test-cred-refresh/",
        repair_check_interval_ms=1000,
        overrides=S3Overrides(
            endpoint_url=endpoint_url,
            credentials=counting_provider,
            force_path_style=True,
        ),
    )

    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )
    with client_instance as c:
        c.set("refresh-key", {"data": "refresh-value"})

        # The provider should have been called at least once
        assert call_count >= 1, f"Expected provider to be called at least once, got {call_count}"


def test_credential_provider_with_expires_at(s3_bucket: str) -> None:
    """Test that expires_at on credentials is converted to ISO 8601 expiry_time correctly."""
    from datetime import datetime, timedelta, timezone

    endpoint_url = os.getenv("IMMUKV_S3_ENDPOINT", "http://localhost:9000")
    access_key = os.getenv("AWS_ACCESS_KEY_ID", "test")
    secret_key = os.getenv("AWS_SECRET_ACCESS_KEY", "test")

    # Set expiry 30 seconds from now
    expires_at = datetime.now(timezone.utc) + timedelta(seconds=30)

    async def expiring_provider() -> S3Credentials:
        return S3Credentials(
            aws_access_key_id=access_key,
            aws_secret_access_key=secret_key,
            expires_at=expires_at,
        )

    config = Config(
        s3_bucket=s3_bucket,
        s3_region="us-east-1",
        s3_prefix="test-cred-expires/",
        repair_check_interval_ms=1000,
        overrides=S3Overrides(
            endpoint_url=endpoint_url,
            credentials=expiring_provider,
            force_path_style=True,
        ),
    )

    client_instance: ImmuKVClient[str, object] = ImmuKVClient(
        config, identity_decoder, identity_encoder
    )
    with client_instance as c:
        # Should work fine with near-future expiry
        entry = c.set("expiry-key", {"data": "expiry-value"})
        assert entry.key == "expiry-key"
        assert entry.value == {"data": "expiry-value"}

        retrieved = c.get("expiry-key")
        assert retrieved.value == {"data": "expiry-value"}
