"""
Tests for the audit log feature.

Tests the audit log list, stats, filtering, and pagination endpoints.
Verifies that audit entries are created for operations when audit logging is enabled.
"""

import asyncio

import httpx
import pytest
import pytest_asyncio

from hindsight_api.api import create_app
from hindsight_api.config import get_config


@pytest_asyncio.fixture
async def audit_api_client(memory):
    """Create a test client with audit logging enabled."""
    # Enable audit logging on the memory engine's audit logger
    memory._audit_logger._enabled = True
    memory._audit_logger._allowed_actions = None  # All actions

    app = create_app(memory, initialize_memory=False)
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        yield client


@pytest.fixture
def bank_id():
    """Provide a unique bank ID for audit tests."""
    from datetime import datetime

    return f"audit_test_{datetime.now().timestamp()}"


@pytest.mark.asyncio
async def test_audit_log_list_empty(audit_api_client, bank_id):
    """Test listing audit logs for a bank with no entries returns empty."""
    # Create the bank first
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    # Small delay for fire-and-forget audit writes
    await asyncio.sleep(0.5)

    response = await audit_api_client.get(f"/v1/default/banks/{bank_id}/audit-logs")
    assert response.status_code == 200
    data = response.json()
    assert data["bank_id"] == bank_id
    assert "total" in data
    assert "items" in data
    assert "limit" in data
    assert "offset" in data
    assert isinstance(data["items"], list)


@pytest.mark.asyncio
async def test_audit_log_created_for_retain(audit_api_client, bank_id):
    """Test that a retain operation creates an audit log entry."""
    # Create bank
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    # Perform a retain
    response = await audit_api_client.post(
        f"/v1/default/banks/{bank_id}/memories",
        json={
            "items": [{"content": "Alice likes cats", "context": "preferences"}],
        },
    )
    assert response.status_code == 200

    # Wait for fire-and-forget audit writes
    await asyncio.sleep(1.0)

    # List audit logs - should have entries for create_bank and retain
    response = await audit_api_client.get(f"/v1/default/banks/{bank_id}/audit-logs")
    assert response.status_code == 200
    data = response.json()
    assert data["total"] >= 1

    actions = [item["action"] for item in data["items"]]
    assert "retain" in actions, f"Expected 'retain' in audit actions, got: {actions}"


@pytest.mark.asyncio
async def test_audit_log_entry_fields(audit_api_client, bank_id):
    """Test that audit log entries have all expected fields."""
    # Create bank + recall to generate entries
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    await audit_api_client.post(
        f"/v1/default/banks/{bank_id}/memories/recall",
        json={"query": "test query"},
    )

    await asyncio.sleep(1.0)

    response = await audit_api_client.get(f"/v1/default/banks/{bank_id}/audit-logs")
    assert response.status_code == 200
    data = response.json()
    assert data["total"] >= 1

    # Check the recall entry has all fields
    recall_entries = [item for item in data["items"] if item["action"] == "recall"]
    assert len(recall_entries) >= 1, f"Expected recall entry, got actions: {[i['action'] for i in data['items']]}"

    entry = recall_entries[0]
    assert entry["id"] is not None
    assert entry["action"] == "recall"
    assert entry["transport"] == "http"
    assert entry["bank_id"] == bank_id
    assert entry["started_at"] is not None
    assert entry["ended_at"] is not None
    # Request should contain the recall parameters
    assert entry["request"] is not None
    assert "query" in entry["request"]
    # Response should contain the recall results
    assert entry["response"] is not None


@pytest.mark.asyncio
async def test_audit_log_filter_by_action(audit_api_client, bank_id):
    """Test filtering audit logs by action type."""
    # Create bank and do retain + recall
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )
    await audit_api_client.post(
        f"/v1/default/banks/{bank_id}/memories",
        json={"items": [{"content": "test content", "context": "test"}]},
    )
    await audit_api_client.post(
        f"/v1/default/banks/{bank_id}/memories/recall",
        json={"query": "test"},
    )

    await asyncio.sleep(1.0)

    # Filter by retain only
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"action": "retain"},
    )
    assert response.status_code == 200
    data = response.json()
    for item in data["items"]:
        assert item["action"] == "retain"

    # Filter by recall only
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"action": "recall"},
    )
    assert response.status_code == 200
    data = response.json()
    for item in data["items"]:
        assert item["action"] == "recall"


@pytest.mark.asyncio
async def test_audit_log_filter_by_transport(audit_api_client, bank_id):
    """Test filtering audit logs by transport type."""
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    await asyncio.sleep(0.5)

    # Filter by http transport
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"transport": "http"},
    )
    assert response.status_code == 200
    data = response.json()
    for item in data["items"]:
        assert item["transport"] == "http"

    # Filter by mcp transport - should be empty (no MCP calls in this test)
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"transport": "mcp"},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["total"] == 0


@pytest.mark.asyncio
async def test_audit_log_filter_by_date_range(audit_api_client, bank_id):
    """Test filtering audit logs by date range."""
    from datetime import datetime, timedelta, timezone

    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    await asyncio.sleep(0.5)

    now = datetime.now(timezone.utc)

    # Filter with start_date in the past - should include entries
    past = (now - timedelta(hours=1)).isoformat()
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"start_date": past},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["total"] >= 1

    # Filter with start_date in the future - should be empty
    future = (now + timedelta(hours=1)).isoformat()
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"start_date": future},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["total"] == 0


@pytest.mark.asyncio
async def test_audit_log_pagination(audit_api_client, bank_id):
    """Test audit log pagination with limit and offset."""
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    # Generate multiple audit entries
    for i in range(5):
        await audit_api_client.post(
            f"/v1/default/banks/{bank_id}/memories/recall",
            json={"query": f"test query {i}"},
        )

    await asyncio.sleep(1.5)

    # Get first page
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"limit": 2, "offset": 0},
    )
    assert response.status_code == 200
    page1 = response.json()
    assert len(page1["items"]) == 2
    assert page1["limit"] == 2
    assert page1["offset"] == 0
    assert page1["total"] >= 5  # At least 5 recall + 1 create_bank

    # Get second page
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs",
        params={"limit": 2, "offset": 2},
    )
    assert response.status_code == 200
    page2 = response.json()
    assert len(page2["items"]) == 2
    assert page2["offset"] == 2

    # Entries should be different between pages
    page1_ids = {item["id"] for item in page1["items"]}
    page2_ids = {item["id"] for item in page2["items"]}
    assert page1_ids.isdisjoint(page2_ids), "Pages should not overlap"


@pytest.mark.asyncio
async def test_audit_log_stats(audit_api_client, bank_id):
    """Test the audit log stats endpoint returns correct structure."""
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    await audit_api_client.post(
        f"/v1/default/banks/{bank_id}/memories/recall",
        json={"query": "stats test"},
    )

    await asyncio.sleep(1.0)

    # Get stats for last 24h
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs/stats",
        params={"period": "1d"},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["bank_id"] == bank_id
    assert data["period"] == "1d"
    assert data["trunc"] == "day"
    assert "buckets" in data
    assert isinstance(data["buckets"], list)

    # Should have at least one bucket with our operations
    assert len(data["buckets"]) >= 1
    bucket = data["buckets"][0]
    assert "time" in bucket
    assert "actions" in bucket
    assert "total" in bucket
    assert bucket["total"] >= 1


@pytest.mark.asyncio
async def test_audit_log_stats_filter_by_action(audit_api_client, bank_id):
    """Test stats endpoint filters by action."""
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    await audit_api_client.post(
        f"/v1/default/banks/{bank_id}/memories/recall",
        json={"query": "test"},
    )

    await asyncio.sleep(1.0)

    # Stats filtered by recall
    response = await audit_api_client.get(
        f"/v1/default/banks/{bank_id}/audit-logs/stats",
        params={"period": "1d", "action": "recall"},
    )
    assert response.status_code == 200
    data = response.json()
    for bucket in data["buckets"]:
        # All actions in buckets should be "recall" only
        for action_name in bucket["actions"]:
            assert action_name == "recall"


@pytest.mark.asyncio
async def test_audit_log_stats_periods(audit_api_client, bank_id):
    """Test stats endpoint supports different periods."""
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Audit Test Bank"},
    )

    await asyncio.sleep(0.5)

    for period, expected_trunc in [("1d", "day"), ("7d", "day"), ("30d", "day")]:
        response = await audit_api_client.get(
            f"/v1/default/banks/{bank_id}/audit-logs/stats",
            params={"period": period},
        )
        assert response.status_code == 200
        data = response.json()
        assert data["period"] == period
        assert data["trunc"] == expected_trunc


@pytest.mark.asyncio
async def test_audit_log_disabled(memory):
    """Test that no audit logs are created when audit logging is disabled."""
    # Ensure audit logging is disabled
    memory._audit_logger._enabled = False

    app = create_app(memory, initialize_memory=False)
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        from datetime import datetime

        bid = f"audit_disabled_test_{datetime.now().timestamp()}"

        await client.put(f"/v1/default/banks/{bid}", json={"name": "No Audit"})
        await client.post(
            f"/v1/default/banks/{bid}/memories/recall",
            json={"query": "test"},
        )

        await asyncio.sleep(0.5)

        response = await client.get(f"/v1/default/banks/{bid}/audit-logs")
        assert response.status_code == 200
        data = response.json()
        assert data["total"] == 0, "No audit entries should exist when audit logging is disabled"


@pytest.mark.asyncio
async def test_audit_log_action_allowlist(memory):
    """Test that only allowed actions are audited when allowlist is set."""
    memory._audit_logger._enabled = True
    memory._audit_logger._allowed_actions = frozenset({"recall"})  # Only audit recall

    app = create_app(memory, initialize_memory=False)
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        from datetime import datetime

        bid = f"audit_allowlist_test_{datetime.now().timestamp()}"

        # create_bank should NOT be audited
        await client.put(f"/v1/default/banks/{bid}", json={"name": "Allowlist Test"})
        # recall should be audited
        await client.post(
            f"/v1/default/banks/{bid}/memories/recall",
            json={"query": "allowlist test"},
        )

        await asyncio.sleep(1.0)

        response = await client.get(f"/v1/default/banks/{bid}/audit-logs")
        assert response.status_code == 200
        data = response.json()
        actions = [item["action"] for item in data["items"]]
        assert "recall" in actions, "recall should be audited"
        assert "create_bank" not in actions, "create_bank should NOT be audited (not in allowlist)"


@pytest.mark.asyncio
async def test_audit_log_ordered_by_most_recent(audit_api_client, bank_id):
    """Test that audit logs are returned ordered by most recent first."""
    await audit_api_client.put(
        f"/v1/default/banks/{bank_id}",
        json={"name": "Order Test Bank"},
    )

    for i in range(3):
        await audit_api_client.post(
            f"/v1/default/banks/{bank_id}/memories/recall",
            json={"query": f"order test {i}"},
        )
        await asyncio.sleep(0.2)  # Small gap between requests

    await asyncio.sleep(1.0)

    response = await audit_api_client.get(f"/v1/default/banks/{bank_id}/audit-logs")
    assert response.status_code == 200
    data = response.json()

    # Check descending order by started_at
    timestamps = [item["started_at"] for item in data["items"] if item["started_at"]]
    assert timestamps == sorted(timestamps, reverse=True), "Audit logs should be ordered most recent first"
