"""P2 Security TDD Tests — Medium priority hardening.

Coverage:
  1. PersistentReplayGuard — SQLite tx_hash dedup, prune, concurrent access
  2. Resource exhaustion — large transaction counts, cache limits
  3. Fault injection — corrupted DB, disk errors, malformed data
  4. Gateway integration — 402 challenge, payment flow, replay rejection,
     rate limiting, large body, header whitelist
"""

from __future__ import annotations

import os
import time
from decimal import Decimal
from unittest.mock import AsyncMock, patch

import pytest

# ---------------------------------------------------------------------------
pytestmark = pytest.mark.timeout(15)
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
async def wallet(tmp_path):
    from ag402_core.wallet.agent_wallet import AgentWallet
    db_path = str(tmp_path / "test_p2.db")
    w = AgentWallet(db_path=db_path)
    await w.init_db()
    await w.deposit(1000.0, note="setup")
    yield w
    await w.close()


@pytest.fixture
async def replay_guard(tmp_path):
    from ag402_core.security.replay_guard import PersistentReplayGuard
    db_path = str(tmp_path / "replay_p2.db")
    guard = PersistentReplayGuard(db_path=db_path)
    await guard.init_db()
    yield guard
    await guard.close()


# ===================================================================
# 1. PersistentReplayGuard
# ===================================================================

async def test_persistent_guard_new_tx_accepted(replay_guard):
    """First-time tx_hash should be accepted."""
    assert await replay_guard.check_and_record_tx("tx_hash_unique_1")


async def test_persistent_guard_duplicate_rejected(replay_guard):
    """Same tx_hash twice should be rejected on second attempt."""
    assert await replay_guard.check_and_record_tx("tx_hash_dup")
    assert not await replay_guard.check_and_record_tx("tx_hash_dup")


async def test_persistent_guard_different_hashes(replay_guard):
    """Different tx_hashes should all be accepted."""
    for i in range(10):
        assert await replay_guard.check_and_record_tx(f"tx_{i}")


async def test_persistent_guard_prune_old(replay_guard):
    """Prune should remove old entries and keep recent ones."""
    # Insert with mocked time
    with patch("ag402_core.security.replay_guard.time") as mock_time:
        mock_time.time.return_value = time.time() - 86400 * 10  # 10 days ago
        await replay_guard.check_and_record_tx("old_tx")

    # Insert a recent one
    await replay_guard.check_and_record_tx("recent_tx")

    # Prune (default 7 days)
    pruned = await replay_guard.prune(max_age_seconds=86400 * 7)
    assert pruned >= 1

    # Old tx should now be accepted again (pruned)
    assert await replay_guard.check_and_record_tx("old_tx")

    # Recent tx should still be rejected (not pruned)
    assert not await replay_guard.check_and_record_tx("recent_tx")


async def test_persistent_guard_lazy_init(tmp_path):
    """Guard should auto-initialize DB on first use."""
    from ag402_core.security.replay_guard import PersistentReplayGuard
    guard = PersistentReplayGuard(db_path=str(tmp_path / "lazy.db"))
    # Don't call init_db() — let it lazy-init
    assert await guard.check_and_record_tx("lazy_tx")
    await guard.close()


async def test_persistent_guard_empty_tx_hash(replay_guard):
    """Empty tx_hash should be handled (treated as valid key)."""
    result1 = await replay_guard.check_and_record_tx("")
    assert result1  # first time
    result2 = await replay_guard.check_and_record_tx("")
    assert not result2  # duplicate


# ===================================================================
# 2. Resource Exhaustion
# ===================================================================

async def test_wallet_many_transactions(wallet):
    """Wallet should handle many transactions without performance collapse."""
    # Insert 200 transactions
    for i in range(200):
        await wallet.deduct(Decimal("0.01"), f"addr_{i}", f"hash_{i}")

    # Balance check should still be fast
    balance = await wallet.get_balance()
    assert balance > 0

    # Transaction listing should work
    txs = await wallet.get_transactions(limit=50)
    assert len(txs) == 50


async def test_wallet_summary_stats_with_many_txs(wallet):
    """get_summary_stats should work with many transactions."""
    for i in range(50):
        await wallet.deduct(Decimal("0.01"), f"addr_{i}", f"hash_{i}")

    stats = await wallet.get_summary_stats()
    assert stats["tx_count"] == 50
    assert stats["total_spend"] == Decimal("0.50")
    assert stats["balance"] > 0


async def test_replay_guard_many_nonces():
    """In-memory ReplayGuard should handle cache limits gracefully."""
    from ag402_core.security.replay_guard import ReplayGuard

    guard = ReplayGuard(window_seconds=30, max_cache=100)
    now = time.time()
    accepted = 0
    rejected = 0
    for i in range(150):
        ok, _ = guard.check(str(now), f"nonce_{i}")
        if ok:
            accepted += 1
        else:
            rejected += 1

    # First 100 should be accepted, rest rejected (cache full)
    assert accepted == 100
    assert rejected == 50


# ===================================================================
# 3. Fault Injection
# ===================================================================

def test_wallet_corrupted_db_sync(tmp_path):
    """Opening a corrupted DB file should raise an error.

    Uses synchronous sqlite3 to verify — aiosqlite hangs on corrupted files.
    This documents the known issue: aiosqlite does not handle corrupted DBs
    gracefully (no timeout / no error).
    """
    import sqlite3

    db_path = str(tmp_path / "corrupted.db")
    with open(db_path, "wb") as f:
        f.write(b"THIS IS NOT A SQLITE DATABASE" * 100)

    conn = sqlite3.connect(db_path)
    with pytest.raises(sqlite3.DatabaseError):
        conn.execute("PRAGMA journal_mode=WAL")
    conn.close()


def test_replay_guard_corrupted_db_sync(tmp_path):
    """PersistentReplayGuard's underlying SQLite should reject corrupted DB.

    Uses synchronous sqlite3 to verify — aiosqlite hangs on corrupted files.
    """
    import sqlite3

    db_path = str(tmp_path / "corrupted_replay.db")
    with open(db_path, "wb") as f:
        f.write(b"NOT A DB" * 50)

    conn = sqlite3.connect(db_path)
    with pytest.raises(sqlite3.DatabaseError):
        conn.execute("CREATE TABLE IF NOT EXISTS test (id TEXT)")
    conn.close()


def test_encryption_empty_password():
    """Encrypting with empty password should raise or be rejected."""
    from ag402_core.security.wallet_encryption import encrypt_private_key

    # Empty password: should raise ValueError (password strength validation)
    with pytest.raises(ValueError):
        encrypt_private_key("", "my_key")


def test_encryption_none_key():
    """Encrypting None key should raise."""
    from ag402_core.security.wallet_encryption import encrypt_private_key

    with pytest.raises((TypeError, AttributeError)):
        encrypt_private_key("password123456", None)


async def test_wallet_double_init(tmp_path):
    """Calling close + init_db again should not corrupt the database."""
    from ag402_core.wallet.agent_wallet import AgentWallet

    db_path = str(tmp_path / "double_init.db")
    w = AgentWallet(db_path=db_path)
    await w.init_db()
    await w.deposit(50.0, note="first")
    await w.close()

    # Re-open same path
    await w.init_db()
    balance = await w.get_balance()
    assert balance == Decimal("50.0")
    await w.close()


async def test_wallet_close_and_reopen(tmp_path):
    """Wallet data should persist across close/reopen cycles."""
    from ag402_core.wallet.agent_wallet import AgentWallet

    db_path = str(tmp_path / "persist.db")
    w = AgentWallet(db_path=db_path)
    await w.init_db()
    await w.deposit(75.0, note="persist test")
    await w.close()

    # Reopen
    w2 = AgentWallet(db_path=db_path)
    await w2.init_db()
    balance = await w2.get_balance()
    assert balance == Decimal("75.0")
    await w2.close()


# ===================================================================
# 4. Gateway Integration Tests
# ===================================================================

@pytest.fixture
def gateway_app(tmp_path):
    """Create a test gateway app."""
    from ag402_mcp.gateway import X402Gateway

    os.environ["X402_MODE"] = "test"
    gw = X402Gateway(
        target_url="http://testserver:9999",
        price="0.05",
        address="TestAddr111111111111111111111111111111111111",
        replay_db_path=str(tmp_path / "gw_replay.db"),
        rate_limit_per_minute=100,
    )
    app = gw.create_app()
    return app, gw


@pytest.fixture
def gateway_client(gateway_app):
    """Create an HTTPX test client for the gateway."""
    from httpx import ASGITransport, AsyncClient

    app, gw = gateway_app
    transport = ASGITransport(app=app)
    client = AsyncClient(transport=transport, base_url="http://test")
    return client, gw


async def test_gateway_health_check(gateway_client):
    """Health endpoint should return 200."""
    client, _ = gateway_client
    resp = await client.get("/health")
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] == "healthy"
    assert "metrics" in data


async def test_gateway_no_auth_returns_402(gateway_client):
    """Request without Authorization should get 402."""
    client, _ = gateway_client
    resp = await client.get("/api/test")
    assert resp.status_code == 402
    assert "WWW-Authenticate" in resp.headers
    assert "x402" in resp.headers["WWW-Authenticate"]


async def test_gateway_non_x402_auth_returns_403(gateway_client):
    """Non-x402 Authorization should get 403."""
    client, _ = gateway_client
    resp = await client.get("/api/test", headers={"Authorization": "Bearer token123"})
    assert resp.status_code == 403


async def test_gateway_x402_without_replay_headers(gateway_client):
    """x402 auth without replay protection headers should be rejected."""
    client, _ = gateway_client
    resp = await client.get(
        "/api/test",
        headers={"Authorization": "x402 fake_tx_hash"}
    )
    assert resp.status_code == 403
    data = resp.json()
    assert "replay" in data.get("error", "").lower() or "replay" in data.get("detail", "").lower()


async def test_gateway_x402_with_replay_headers(tmp_path):
    """x402 auth with valid replay headers should proceed to verification."""
    from ag402_core.security.replay_guard import generate_replay_headers
    from ag402_mcp.gateway import X402Gateway
    from httpx import ASGITransport, AsyncClient

    os.environ["X402_MODE"] = "test"
    gw = X402Gateway(
        target_url="http://testserver:9999",
        price="0.05",
        address="TestAddr111111111111111111111111111111111111",
        replay_db_path=str(tmp_path / "replay_hdr.db"),
    )

    # Mock verifier to return valid immediately (avoids async hang)
    gw.verifier.verify = AsyncMock(return_value=type("R", (), {
        "valid": True, "tx_hash": "valid_tx_123", "error": ""
    })())

    # Pre-init persistent guard (lifespan doesn't run in test transport)
    await gw._persistent_guard.init_db()

    app = gw.create_app()
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        replay_hdrs = generate_replay_headers()
        resp = await client.get(
            "/api/test",
            headers={
                "Authorization": "x402 valid_tx_hash_123",
                **replay_hdrs,
            }
        )
        # Should pass replay check and reach verification
        # (502 from proxy failing is acceptable — means payment was verified)
        assert resp.status_code != 403 or "replay" not in resp.json().get("detail", "").lower()

    await gw._persistent_guard.close()


async def test_gateway_rate_limit(tmp_path):
    """Exceeding rate limit should return 429."""
    from ag402_mcp.gateway import X402Gateway
    from httpx import ASGITransport, AsyncClient

    os.environ["X402_MODE"] = "test"
    gw = X402Gateway(
        target_url="http://testserver:9999",
        price="0.05",
        address="TestAddr111111111111111111111111111111111111",
        replay_db_path=str(tmp_path / "rl.db"),
        rate_limit_per_minute=3,  # Very low limit
    )
    app = gw.create_app()
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        # First 3 requests should work (402 or other, but not 429)
        for i in range(3):
            resp = await client.get(f"/api/test{i}")
            assert resp.status_code != 429, f"Request {i} should not be rate-limited"

        # 4th request should be rate-limited
        resp = await client.get("/api/test_overflow")
        assert resp.status_code == 429


async def test_gateway_metrics_increment(gateway_client):
    """Metrics should increment on requests."""
    client, gw = gateway_client

    # Initial state
    assert gw._metrics["requests_total"] == 0

    # Make a request
    await client.get("/api/test")
    assert gw._metrics["requests_total"] >= 1
    assert gw._metrics["challenges_issued"] >= 1


async def test_gateway_header_whitelist(tmp_path):
    """Dangerous headers should not be forwarded to upstream."""
    from ag402_core.security.replay_guard import generate_replay_headers
    from ag402_mcp.gateway import X402Gateway
    from httpx import ASGITransport, AsyncClient

    os.environ["X402_MODE"] = "test"
    gw = X402Gateway(
        target_url="http://testserver:9999",
        price="0.05",
        address="TestAddr111111111111111111111111111111111111",
        replay_db_path=str(tmp_path / "wl.db"),
    )

    # Mock verifier to accept payment
    gw.verifier.verify = AsyncMock(return_value=type("R", (), {
        "valid": True, "tx_hash": "mock_wl_tx", "error": ""
    })())

    # Mock the proxy to return success (avoids actual HTTP call)
    async def mock_proxy(request, path):
        from fastapi.responses import Response
        return Response(content=b"ok", status_code=200)

    gw._proxy_request = mock_proxy

    # Pre-init persistent guard (lifespan doesn't run in test transport)
    await gw._persistent_guard.init_db()

    app = gw.create_app()
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        replay_hdrs = generate_replay_headers()
        resp = await client.get(
            "/api/test",
            headers={
                "Authorization": "x402 mock_tx_hash",
                "Cookie": "session=evil",
                "X-Forwarded-For": "spoofed",
                "Connection": "keep-alive",
                **replay_hdrs,
            }
        )
        assert resp.status_code == 200

    await gw._persistent_guard.close()
