from __future__ import annotations

import tempfile
from pathlib import Path
from typing import cast

import pytest

pytest.importorskip("cryptography")  # Skip tests if cryptography is not installed

from cryptography.fernet import Fernet

from agents import Agent, Runner, SessionSettings, SQLiteSession, TResponseInputItem
from agents.extensions.memory.encrypt_session import EncryptedSession
from tests.fake_model import FakeModel
from tests.test_responses import get_text_message

# Mark all tests in this file as asyncio
pytestmark = pytest.mark.asyncio


def _invalid_encrypted_envelope() -> TResponseInputItem:
    return cast(
        TResponseInputItem,
        {"__enc__": 1, "v": 1, "kid": "hkdf-v1", "payload": "not-a-valid-token"},
    )


@pytest.fixture
def agent() -> Agent:
    """Fixture for a basic agent with a fake model."""
    return Agent(name="test", model=FakeModel())


@pytest.fixture
def encryption_key() -> str:
    """Fixture for a valid Fernet encryption key."""
    return str(Fernet.generate_key().decode("utf-8"))


@pytest.fixture
def set_fernet_time(monkeypatch):
    """Freeze Fernet TTL checks so expiration tests avoid real waiting."""
    current_time = 1_000

    def _set_time(value: int) -> None:
        nonlocal current_time
        current_time = value

    monkeypatch.setattr("cryptography.fernet.time.time", lambda: current_time)
    return _set_time


@pytest.fixture
def underlying_session():
    """Fixture for an underlying SQLite session."""
    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_encrypt.db"
    return SQLiteSession("test_session", db_path)


async def test_encrypted_session_basic_functionality(
    agent: Agent, encryption_key: str, underlying_session: SQLiteSession
):
    """Test basic encryption/decryption functionality."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
        ttl=600,
    )

    items: list[TResponseInputItem] = [
        {"role": "user", "content": "Hello"},
        {"role": "assistant", "content": "Hi there!"},
    ]
    await session.add_items(items)

    retrieved = await session.get_items()
    assert len(retrieved) == 2
    assert retrieved[0].get("content") == "Hello"
    assert retrieved[1].get("content") == "Hi there!"

    encrypted_items = await underlying_session.get_items()
    assert encrypted_items[0].get("__enc__") == 1
    assert "payload" in encrypted_items[0]
    assert encrypted_items[0].get("content") != "Hello"

    underlying_session.close()


async def test_encrypted_session_with_runner(
    agent: Agent, encryption_key: str, underlying_session: SQLiteSession
):
    """Test that EncryptedSession works with Runner."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    assert isinstance(agent.model, FakeModel)
    agent.model.set_next_output([get_text_message("San Francisco")])
    result1 = await Runner.run(
        agent,
        "What city is the Golden Gate Bridge in?",
        session=session,
    )
    assert result1.final_output == "San Francisco"

    agent.model.set_next_output([get_text_message("California")])
    result2 = await Runner.run(agent, "What state is it in?", session=session)
    assert result2.final_output == "California"

    last_input = agent.model.last_turn_args["input"]
    assert len(last_input) > 1
    assert any("Golden Gate Bridge" in str(item.get("content", "")) for item in last_input)

    underlying_session.close()


async def test_encrypted_session_pop_item(encryption_key: str, underlying_session: SQLiteSession):
    """Test pop_item functionality."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    items: list[TResponseInputItem] = [
        {"role": "user", "content": "First"},
        {"role": "assistant", "content": "Second"},
    ]
    await session.add_items(items)

    popped = await session.pop_item()
    assert popped is not None
    assert popped.get("content") == "Second"

    remaining = await session.get_items()
    assert len(remaining) == 1
    assert remaining[0].get("content") == "First"

    underlying_session.close()


async def test_encrypted_session_clear(encryption_key: str, underlying_session: SQLiteSession):
    """Test clear_session functionality."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    await session.add_items([{"role": "user", "content": "Test"}])
    await session.clear_session()

    items = await session.get_items()
    assert len(items) == 0

    underlying_session.close()


async def test_encrypted_session_ttl_expiration(
    encryption_key: str, underlying_session: SQLiteSession, set_fernet_time
):
    """Test TTL expiration - expired items are silently skipped."""
    set_fernet_time(1_000)
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
        ttl=1,  # 1 second TTL
    )

    items: list[TResponseInputItem] = [
        {"role": "user", "content": "Hello"},
        {"role": "assistant", "content": "Hi"},
    ]
    await session.add_items(items)

    set_fernet_time(1_002)

    retrieved = await session.get_items()
    assert len(retrieved) == 0

    underlying_items = await underlying_session.get_items()
    assert len(underlying_items) == 2

    underlying_session.close()


async def test_encrypted_session_pop_expired(
    encryption_key: str, underlying_session: SQLiteSession, set_fernet_time
):
    """Test pop_item with expired data."""
    set_fernet_time(1_000)
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
        ttl=1,
    )

    await session.add_items([{"role": "user", "content": "Test"}])
    set_fernet_time(1_002)

    popped = await session.pop_item()
    assert popped is None

    underlying_session.close()


async def test_encrypted_session_pop_mixed_expired_valid(
    encryption_key: str, underlying_session: SQLiteSession, set_fernet_time
):
    """Test pop_item auto-retry with mixed expired and valid items."""
    set_fernet_time(1_000)
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
        ttl=2,  # 2 second TTL
    )

    await session.add_items(
        [
            {"role": "user", "content": "Old message 1"},
            {"role": "assistant", "content": "Old response 1"},
        ]
    )

    set_fernet_time(1_003)

    await session.add_items(
        [
            {"role": "user", "content": "New message"},
            {"role": "assistant", "content": "New response"},
        ]
    )

    popped = await session.pop_item()
    assert popped is not None
    assert popped.get("content") == "New response"

    popped2 = await session.pop_item()
    assert popped2 is not None
    assert popped2.get("content") == "New message"

    popped3 = await session.pop_item()
    assert popped3 is None

    underlying_session.close()


async def test_encrypted_session_raw_string_key(underlying_session: SQLiteSession):
    """Test using raw string as encryption key (not base64)."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key="my-secret-password",  # Raw string, not Fernet key
    )

    await session.add_items([{"role": "user", "content": "Test"}])
    items = await session.get_items()
    assert len(items) == 1
    assert items[0].get("content") == "Test"

    underlying_session.close()


async def test_encrypted_session_get_items_limit(
    encryption_key: str, underlying_session: SQLiteSession
):
    """Test get_items with limit parameter."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    items: list[TResponseInputItem] = [
        {"role": "user", "content": f"Message {i}"} for i in range(5)
    ]
    await session.add_items(items)

    limited = await session.get_items(limit=2)
    assert len(limited) == 2
    assert limited[0].get("content") == "Message 3"  # Latest 2
    assert limited[1].get("content") == "Message 4"

    underlying_session.close()


async def test_encrypted_session_get_items_limit_skips_invalid_latest_envelope(
    encryption_key: str, underlying_session: SQLiteSession
):
    """Test that limit counts valid decrypted items, not encrypted envelopes."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    await session.add_items([{"role": "user", "content": "older valid"}])
    await underlying_session.add_items([_invalid_encrypted_envelope()])

    all_items = await session.get_items()
    assert [item.get("content") for item in all_items] == ["older valid"]

    limited = await session.get_items(limit=1)
    assert [item.get("content") for item in limited] == ["older valid"]

    underlying_session.close()


async def test_encrypted_session_get_items_limit_returns_latest_valid_items_after_invalids(
    encryption_key: str, underlying_session: SQLiteSession
):
    """Test that invalid envelopes do not hide earlier valid items from limit."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    await session.add_items(
        [
            {"role": "user", "content": "valid 0"},
            {"role": "assistant", "content": "valid 1"},
        ]
    )
    await underlying_session.add_items([_invalid_encrypted_envelope()])
    await session.add_items([{"role": "user", "content": "valid 2"}])

    limited = await session.get_items(limit=2)
    assert [item.get("content") for item in limited] == ["valid 1", "valid 2"]

    underlying_session.close()


async def test_encrypted_session_get_items_session_settings_limit_skips_invalid_envelopes(
    encryption_key: str, underlying_session: SQLiteSession
):
    """Test that session settings limit counts valid decrypted items."""
    underlying_session.session_settings = SessionSettings(limit=3)
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    await session.add_items(
        [
            {"role": "user", "content": "valid 0"},
            {"role": "assistant", "content": "valid 1"},
            {"role": "user", "content": "valid 2"},
        ]
    )
    await underlying_session.add_items([_invalid_encrypted_envelope()])

    items = await session.get_items()
    assert [item.get("content") for item in items] == ["valid 0", "valid 1", "valid 2"]

    underlying_session.close()


async def test_encrypted_session_unicode_content(
    encryption_key: str, underlying_session: SQLiteSession
):
    """Test encryption of international text content."""
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    items: list[TResponseInputItem] = [
        {"role": "user", "content": "Hello world"},
        {"role": "assistant", "content": "Special chars: áéíóú"},
        {"role": "user", "content": "Numbers and symbols: 123!@#"},
    ]
    await session.add_items(items)

    retrieved = await session.get_items()
    assert retrieved[0].get("content") == "Hello world"
    assert retrieved[1].get("content") == "Special chars: áéíóú"
    assert retrieved[2].get("content") == "Numbers and symbols: 123!@#"

    underlying_session.close()


class CustomSession(SQLiteSession):
    """Mock custom session with additional methods for testing delegation."""

    def get_stats(self) -> dict[str, int]:
        """Custom method that should be accessible through delegation."""
        return {"custom_method_calls": 42, "test_value": 123}

    async def custom_async_method(self) -> str:
        """Custom async method for testing delegation."""
        return "custom_async_result"


async def test_encrypted_session_delegation():
    """Test that custom methods on underlying session are accessible through delegation."""
    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_delegation.db"
    underlying_session = CustomSession("test_session", db_path)

    encryption_key = str(Fernet.generate_key().decode("utf-8"))
    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying_session,
        encryption_key=encryption_key,
    )

    stats = session.get_stats()
    assert stats == {"custom_method_calls": 42, "test_value": 123}

    result = await session.custom_async_method()
    assert result == "custom_async_result"

    await session.add_items([{"role": "user", "content": "Test delegation"}])
    items = await session.get_items()
    assert len(items) == 1
    assert items[0].get("content") == "Test delegation"

    underlying_session.close()


# ============================================================================
# SessionSettings Tests
# ============================================================================


async def test_session_settings_delegated_to_underlying(encryption_key: str):
    """Test that session_settings is correctly delegated to underlying session."""
    from agents.memory import SessionSettings

    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_settings.db"
    underlying = SQLiteSession("test_session", db_path, session_settings=SessionSettings(limit=5))

    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying,
        encryption_key=encryption_key,
    )

    # session_settings should be accessible through EncryptedSession
    assert session.session_settings is not None
    assert session.session_settings.limit == 5

    underlying.close()


async def test_session_settings_get_items_uses_underlying_limit(encryption_key: str):
    """Test that get_items uses underlying session's session_settings.limit."""
    from agents.memory import SessionSettings

    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_settings_limit.db"
    underlying = SQLiteSession("test_session", db_path, session_settings=SessionSettings(limit=3))

    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying,
        encryption_key=encryption_key,
    )

    # Add 5 items
    items: list[TResponseInputItem] = [
        {"role": "user", "content": f"Message {i}"} for i in range(5)
    ]
    await session.add_items(items)

    # get_items() with no limit should use underlying session_settings.limit=3
    retrieved = await session.get_items()
    assert len(retrieved) == 3
    # Should get the last 3 items
    assert retrieved[0].get("content") == "Message 2"
    assert retrieved[1].get("content") == "Message 3"
    assert retrieved[2].get("content") == "Message 4"

    underlying.close()


async def test_session_settings_explicit_limit_overrides_settings(encryption_key: str):
    """Test that explicit limit parameter overrides session_settings."""
    from agents.memory import SessionSettings

    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_override.db"
    underlying = SQLiteSession("test_session", db_path, session_settings=SessionSettings(limit=5))

    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying,
        encryption_key=encryption_key,
    )

    # Add 10 items
    items: list[TResponseInputItem] = [
        {"role": "user", "content": f"Message {i}"} for i in range(10)
    ]
    await session.add_items(items)

    # Explicit limit=2 should override session_settings.limit=5
    retrieved = await session.get_items(limit=2)
    assert len(retrieved) == 2
    assert retrieved[0].get("content") == "Message 8"
    assert retrieved[1].get("content") == "Message 9"

    underlying.close()


async def test_session_settings_resolve():
    """Test SessionSettings.resolve() method."""
    from agents.memory import SessionSettings

    base = SessionSettings(limit=100)
    override = SessionSettings(limit=50)

    final = base.resolve(override)

    assert final.limit == 50  # Override wins
    assert base.limit == 100  # Original unchanged

    # Resolving with None returns self
    final_none = base.resolve(None)
    assert final_none.limit == 100


async def test_runner_with_session_settings_override(encryption_key: str):
    """Test that RunConfig can override session's default settings."""
    from agents import Agent, RunConfig, Runner
    from agents.memory import SessionSettings
    from tests.fake_model import FakeModel
    from tests.test_responses import get_text_message

    temp_dir = tempfile.mkdtemp()
    db_path = Path(temp_dir) / "test_runner_override.db"
    underlying = SQLiteSession("test_session", db_path, session_settings=SessionSettings(limit=100))

    session = EncryptedSession(
        session_id="test_session",
        underlying_session=underlying,
        encryption_key=encryption_key,
    )

    # Add some history
    items: list[TResponseInputItem] = [{"role": "user", "content": f"Turn {i}"} for i in range(10)]
    await session.add_items(items)

    model = FakeModel()
    agent = Agent(name="test", model=model)
    model.set_next_output([get_text_message("Got it")])

    await Runner.run(
        agent,
        "New question",
        session=session,
        run_config=RunConfig(
            session_settings=SessionSettings(limit=2)  # Override to 2
        ),
    )

    # Verify the agent received only the last 2 history items + new question
    last_input = model.last_turn_args["input"]
    # Filter out the new "New question" input
    history_items = [item for item in last_input if item.get("content") != "New question"]
    # Should have 2 history items (last two from the 10 we added)
    assert len(history_items) == 2

    underlying.close()
