"""E2E tests for Operations REST API with permission enforcement.

Tests issue #1197: Verify GET /api/v2/operations works correctly when:
Tests issue #1198: Verify GET /api/v2/operations/agents/{agent_id}/activity

Both tests run with:
- auth_type = database (API keys stored in DB, JWT tokens)
- enforce_permissions = true (ReBAC permission checks enabled)

Tests both admin and unauthenticated access.
"""

import os
import signal
import socket
import subprocess
import sys
import time
from contextlib import closing, suppress
from pathlib import Path

import httpx
import pytest

from nexus.contracts.constants import ROOT_ZONE_ID

_src_path = Path(__file__).parent.parent.parent / "src"


def _find_free_port() -> int:
    with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
        s.bind(("", 0))
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        return s.getsockname()[1]


def _wait_for_server(url: str, timeout: float = 60.0) -> bool:
    start = time.time()
    while time.time() - start < timeout:
        try:
            response = httpx.get(f"{url}/health", timeout=2.0, trust_env=False)
            if response.status_code == 200:
                return True
        except (httpx.ConnectError, httpx.ReadTimeout):
            pass
        time.sleep(0.3)
    return False


# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture(scope="module")
def operations_server(tmp_path_factory):
    """Start nexus server with database auth and permission enforcement."""
    tmp_path = tmp_path_factory.mktemp("ops_e2e")
    db_path = tmp_path / "nexus.db"
    storage_path = tmp_path / "storage"
    storage_path.mkdir(exist_ok=True)

    port = _find_free_port()
    base_url = f"http://127.0.0.1:{port}"

    env = {
        k: v
        for k, v in os.environ.items()
        if k not in ("CONDA_PREFIX", "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy")
    }
    env.update(
        {
            "NEXUS_JWT_SECRET": "test-jwt-secret-for-ops-e2e",
            "NEXUS_DATABASE_URL": f"sqlite:///{db_path}",
            "PYTHONPATH": str(_src_path),
            "NO_PROXY": "*",
            "NEXUS_ENFORCE_PERMISSIONS": "true",
            "NEXUS_ENFORCE_ZONE_ISOLATION": "true",
            "NEXUS_SEARCH_DAEMON": "false",
            "NEXUS_RATE_LIMIT_ENABLED": "false",
        }
    )

    # Pre-create database tables
    from sqlalchemy import create_engine

    from nexus.storage.models import Base

    engine = create_engine(f"sqlite:///{db_path}")
    Base.metadata.create_all(engine)
    engine.dispose()

    # Start server with database auth + init
    process = subprocess.Popen(
        [
            sys.executable,
            "-c",
            (
                f"from nexus.daemon.main import main; "
                f"main(['--host', '127.0.0.1', '--port', '{port}', "
                f"'--data-dir', '{tmp_path}', "
                f"'--auth-type', 'database', '--init'])"
            ),
        ],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        preexec_fn=os.setsid if sys.platform != "win32" else None,
    )

    if not _wait_for_server(base_url, timeout=60.0):
        process.terminate()
        try:
            stdout = process.communicate(timeout=5)[0]
        except subprocess.TimeoutExpired:
            process.kill()
            stdout = process.communicate()[0]
        pytest.fail(f"Server failed to start on port {port}.\nOutput:\n{stdout}")

    # Create admin API key from the database directly
    api_key = None
    try:
        from sqlalchemy import create_engine as ce2
        from sqlalchemy.orm import sessionmaker as sm2

        from nexus.bricks.auth.providers.database_key import DatabaseAPIKeyAuth

        engine2 = ce2(f"sqlite:///{db_path}")
        Session2 = sm2(bind=engine2)
        with Session2() as session:
            _, api_key = DatabaseAPIKeyAuth.create_key(
                session,
                user_id="admin",
                name="Operations E2E key",
                zone_id=ROOT_ZONE_ID,
                is_admin=True,
            )
            session.commit()
        engine2.dispose()
    except Exception as e:
        if sys.platform != "win32":
            with suppress(ProcessLookupError, PermissionError):
                os.killpg(os.getpgid(process.pid), signal.SIGTERM)
        else:
            process.terminate()
        try:
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()
            process.wait()
        pytest.fail(f"Failed to create API key: {e}")

    yield {
        "port": port,
        "base_url": base_url,
        "process": process,
        "api_key": api_key,
        "db_path": db_path,
    }

    # Cleanup
    if sys.platform != "win32":
        with suppress(ProcessLookupError, PermissionError):
            os.killpg(os.getpgid(process.pid), signal.SIGTERM)
    else:
        process.terminate()
    try:
        process.wait(timeout=5)
    except subprocess.TimeoutExpired:
        process.kill()
        process.wait()


@pytest.fixture(scope="module")
def client(operations_server) -> httpx.Client:
    """HTTP client with admin API key."""
    headers = {"Authorization": f"Bearer {operations_server['api_key']}"}
    with httpx.Client(
        base_url=operations_server["base_url"],
        timeout=30.0,
        trust_env=False,
        headers=headers,
    ) as c:
        yield c


@pytest.fixture(scope="module")
def unauthenticated_client(operations_server) -> httpx.Client:
    """HTTP client WITHOUT auth headers."""
    with httpx.Client(
        base_url=operations_server["base_url"],
        timeout=30.0,
        trust_env=False,
    ) as c:
        yield c


# =============================================================================
# Helper: seed operations via the file API
# =============================================================================


def _seed_file_operations(client: httpx.Client) -> None:
    """Write a few files to create operation log entries."""
    for i in range(3):
        resp = client.put(
            f"/v2/write?path=/e2e-test/file-{i}.txt",
            content=f"e2e test content {i}",
            headers={**client.headers, "Content-Type": "application/octet-stream"},
        )
        # Accept 200 (success) or 4xx/5xx (if write endpoint differs)
        # The key is to generate operations in the log
        if resp.status_code not in (200, 201):
            # Try alternate write path used by older server versions
            client.post(
                "/rpc",
                json={
                    "method": "sys_write",
                    "params": {
                        "path": f"/e2e-test/file-{i}.txt",
                        "content": f"e2e test content {i}",
                    },
                },
            )


# =============================================================================
# Tests
# =============================================================================


class TestOperationsAuthEnforcement:
    """Verify auth is enforced on operations endpoint."""

    def test_operations_requires_auth(self, unauthenticated_client: httpx.Client):
        """GET /api/v2/operations without auth returns 401."""
        resp = unauthenticated_client.get("/api/v2/operations")
        assert resp.status_code == 401, f"Expected 401, got {resp.status_code}: {resp.text}"


class TestOperationsEndpoint:
    """E2E tests for GET /api/v2/operations with real server."""

    def test_operations_endpoint_exists(self, client: httpx.Client):
        """GET /api/v2/operations returns 200 (even with no operations)."""
        resp = client.get("/api/v2/operations")
        assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.text}"
        data = resp.json()
        assert "operations" in data
        assert "limit" in data
        assert isinstance(data["operations"], list)

    def test_operations_offset_mode_response_shape(self, client: httpx.Client):
        """Offset mode response includes offset, limit, has_more."""
        resp = client.get("/api/v2/operations?limit=5&offset=0")
        assert resp.status_code == 200
        data = resp.json()
        assert data["offset"] == 0
        assert data["limit"] == 5
        assert "has_more" in data
        assert isinstance(data["has_more"], bool)
        # total is None by default (no COUNT query)
        assert data["total"] is None

    def test_operations_include_total(self, client: httpx.Client):
        """include_total=true returns exact count."""
        resp = client.get("/api/v2/operations?limit=5&include_total=true")
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data["total"], int)

    def test_operations_with_file_writes(self, client: httpx.Client):
        """After writing files, operations appear in the log."""
        # Seed some file operations
        _seed_file_operations(client)

        # Query operations
        resp = client.get("/api/v2/operations?limit=100")
        assert resp.status_code == 200
        data = resp.json()
        # Should have at least some operations (even if file writes
        # failed, there may be server startup operations)
        assert isinstance(data["operations"], list)
        assert isinstance(data["has_more"], bool)

    def test_operations_filter_by_operation_type(self, client: httpx.Client):
        """Filter by operation_type query param."""
        resp = client.get("/api/v2/operations?operation_type=write")
        assert resp.status_code == 200
        data = resp.json()
        for op in data["operations"]:
            assert op["operation_type"] == "write"

    def test_operations_filter_by_path_pattern(self, client: httpx.Client):
        """Filter by path_pattern with wildcard."""
        resp = client.get("/api/v2/operations?path_pattern=/e2e-test/*")
        assert resp.status_code == 200
        data = resp.json()
        for op in data["operations"]:
            assert op["path"].startswith("/e2e-test/")

    def test_operations_cursor_mode(self, client: httpx.Client):
        """Cursor mode returns next_cursor."""
        # Get first page in offset mode
        resp1 = client.get("/api/v2/operations?limit=1")
        assert resp1.status_code == 200
        data1 = resp1.json()

        if len(data1["operations"]) > 0:
            # Use the first operation's ID as cursor base
            first_id = data1["operations"][0]["id"]
            resp2 = client.get(f"/api/v2/operations?cursor={first_id}&limit=10")
            assert resp2.status_code == 200
            data2 = resp2.json()
            assert "next_cursor" in data2
            assert "has_more" in data2
            # Cursor mode should not include offset/total
            assert data2.get("offset") is None
            assert data2.get("total") is None

    def test_operations_limit_validation(self, client: httpx.Client):
        """Limit > 1000 returns 422."""
        resp = client.get("/api/v2/operations?limit=2000")
        assert resp.status_code == 422

    def test_operations_response_fields(self, client: httpx.Client):
        """Each operation has required fields."""
        resp = client.get("/api/v2/operations?limit=1")
        assert resp.status_code == 200
        data = resp.json()
        if len(data["operations"]) > 0:
            op = data["operations"][0]
            assert "id" in op
            assert "operation_type" in op
            assert "path" in op
            assert "status" in op
            assert "timestamp" in op


# =============================================================================
# Agent Activity Summary Tests (Issue #1198)
# =============================================================================


class TestAgentActivityAuthEnforcement:
    """Verify auth is enforced on agent activity endpoint."""

    def test_activity_requires_auth(self, unauthenticated_client: httpx.Client):
        """GET /api/v2/operations/agents/{id}/activity without auth returns 401."""
        resp = unauthenticated_client.get("/api/v2/operations/agents/test-agent/activity")
        assert resp.status_code == 401, f"Expected 401, got {resp.status_code}: {resp.text}"


class TestAgentActivityEndpoint:
    """E2E tests for GET /api/v2/operations/agents/{agent_id}/activity."""

    def test_activity_endpoint_exists(self, client: httpx.Client):
        """Endpoint returns 200 even for an agent with no operations."""
        resp = client.get("/api/v2/operations/agents/nonexistent-agent/activity")
        assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.text}"
        data = resp.json()
        assert data["agent_id"] == "nonexistent-agent"
        assert data["total_operations"] == 0
        assert data["operations_by_type"] == {}
        assert data["recent_paths"] == []
        assert data["last_active"] is None
        assert data["first_seen"] is None

    def test_activity_response_shape(self, client: httpx.Client):
        """Response has all required fields with correct types."""
        resp = client.get("/api/v2/operations/agents/any-agent/activity")
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data["agent_id"], str)
        assert isinstance(data["total_operations"], int)
        assert isinstance(data["operations_by_type"], dict)
        assert isinstance(data["recent_paths"], list)

    def test_activity_with_since_parameter(self, client: httpx.Client):
        """Explicit since parameter is accepted."""
        resp = client.get(
            "/api/v2/operations/agents/test-agent/activity?since=2020-01-01T00:00:00Z"
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "total_operations" in data

    def test_activity_after_file_writes(self, client: httpx.Client):
        """After seeding file writes, the admin user's activity is reflected.

        The server runs file writes as the authenticated admin user.
        The agent_id used by the server for file ops may vary, so we
        query the operations list first to find the actual agent_id.
        """
        # Seed operations
        _seed_file_operations(client)

        # Find agent_ids from the operations log
        ops_resp = client.get("/api/v2/operations?limit=10")
        assert ops_resp.status_code == 200
        ops_data = ops_resp.json()

        if len(ops_data["operations"]) > 0:
            # Use the first operation's agent_id (may be None)
            agent_id = ops_data["operations"][0].get("agent_id")
            if agent_id:
                resp = client.get(f"/api/v2/operations/agents/{agent_id}/activity")
                assert resp.status_code == 200
                data = resp.json()
                assert data["agent_id"] == agent_id
                assert data["total_operations"] >= 1
                assert len(data["operations_by_type"]) >= 1

    def test_activity_agent_id_validation(self, client: httpx.Client):
        """Empty agent_id in path returns 422 (validation error)."""
        # FastAPI should reject empty path segment — the route won't match
        # so this becomes a 404 (no route for /agents//activity)
        resp = client.get("/api/v2/operations/agents//activity")
        assert resp.status_code in (404, 422)
