"""Tests for Phase 4 Stream A: cloud connections, secrets, logging, health check."""

from __future__ import annotations

import asyncio
import json
import logging
import os

import pytest

from loom.cloud.connections import (
    _extract_database,
    _extract_password,
    _extract_user,
    _redact_url,
    create_pool,
    create_redis,
)
from loom.cloud.secrets import SECRET_MAP, maybe_load_gcp_secrets
from loom.config import DatabaseConfig, RedisConfig
from loom.logging import JsonFormatter, is_production, setup_logging


# ---------------------------------------------------------------------------
# Connection factory tests
# ---------------------------------------------------------------------------


class TestConnectionFactories:
    async def test_create_pool_via_factory(self, pool):
        """Factory creates a working pool — use the testcontainer DSN via fixture pool."""
        # The pool fixture already creates via asyncpg; verify factory works too
        dsn = pool._connect_args[0] if pool._connect_args else None
        # Just verify the existing pool works as a baseline
        val = await pool.fetchval("SELECT 1")
        assert val == 1

    async def test_create_pool_factory_direct(self):
        """Factory creates working pool using testcontainer DSN."""
        from tests.conftest import _pg_container

        dsn = _pg_container.get_connection_url().replace("+psycopg2", "")
        config = DatabaseConfig(url=dsn, pool_min_size=1, pool_max_size=3)
        p = await create_pool(config)
        try:
            val = await p.fetchval("SELECT 1")
            assert val == 1
        finally:
            await p.close()

    async def test_create_redis_via_factory(self):
        """Factory creates working Redis client using testcontainer."""
        from tests.conftest import _redis_container

        host = _redis_container.get_container_host_ip()
        port = _redis_container.get_exposed_port(6379)
        config = RedisConfig(url=f"redis://{host}:{port}")
        r = await create_redis(config)
        try:
            assert await r.ping()
        finally:
            await r.aclose()


# ---------------------------------------------------------------------------
# DSN parsing tests
# ---------------------------------------------------------------------------


class TestDSNParsing:
    def test_extract_database(self):
        assert _extract_database("postgresql://u:p@host:5432/mydb") == "mydb"

    def test_extract_database_nested_path(self):
        assert _extract_database("postgresql://u:p@host:5432/db") == "db"

    def test_extract_user(self):
        assert _extract_user("postgresql://alice:secret@host:5432/db") == "alice"

    def test_extract_password(self):
        assert _extract_password("postgresql://alice:s3cret@host:5432/db") == "s3cret"

    def test_extract_password_empty(self):
        assert _extract_password("postgresql://alice@host:5432/db") == ""

    def test_redact_url(self):
        url = "postgresql://user:mypassword@host:5432/db"
        redacted = _redact_url(url)
        assert "mypassword" not in redacted
        assert "***" in redacted
        assert "user" in redacted

    def test_redact_url_no_password(self):
        url = "redis://localhost:6379"
        assert _redact_url(url) == url


# ---------------------------------------------------------------------------
# GCP secrets tests
# ---------------------------------------------------------------------------


class TestGCPSecrets:
    def test_maybe_load_gcp_secrets_noop(self, monkeypatch):
        """Returns 0 when LOOM_GCP_PROJECT not set."""
        monkeypatch.delenv("LOOM_GCP_PROJECT", raising=False)
        assert maybe_load_gcp_secrets() == 0

    def test_secret_map_covers_core(self):
        """SECRET_MAP has entries for core secrets."""
        assert "loom-database-url" in SECRET_MAP
        assert "loom-redis-url" in SECRET_MAP
        assert "loom-skills-api-key" in SECRET_MAP
        # Values map to expected env vars
        assert SECRET_MAP["loom-database-url"] == "LOOM_DATABASE_URL"
        assert SECRET_MAP["loom-redis-url"] == "LOOM_REDIS_URL"


# ---------------------------------------------------------------------------
# Logging tests
# ---------------------------------------------------------------------------


class TestLogging:
    def test_json_formatter_output(self):
        """JsonFormatter produces valid JSON with severity field."""
        formatter = JsonFormatter()
        record = logging.LogRecord(
            name="test", level=logging.INFO, pathname="", lineno=0,
            msg="hello world", args=(), exc_info=None,
        )
        output = formatter.format(record)
        parsed = json.loads(output)
        assert parsed["severity"] == "INFO"
        assert parsed["message"] == "hello world"
        assert "timestamp" in parsed
        assert "logger" in parsed

    def test_json_formatter_with_exception(self):
        formatter = JsonFormatter()
        try:
            raise ValueError("test error")
        except ValueError:
            import sys
            record = logging.LogRecord(
                name="test", level=logging.ERROR, pathname="", lineno=0,
                msg="failed", args=(), exc_info=sys.exc_info(),
            )
        output = formatter.format(record)
        parsed = json.loads(output)
        assert "exception" in parsed
        assert "ValueError" in parsed["exception"]

    def test_is_production_k_service(self, monkeypatch):
        """K_SERVICE env var -> production mode."""
        monkeypatch.setenv("K_SERVICE", "loom-mcp")
        monkeypatch.delenv("LOOM_ENV", raising=False)
        assert is_production() is True

    def test_is_production_loom_env(self, monkeypatch):
        """LOOM_ENV=production -> production mode."""
        monkeypatch.delenv("K_SERVICE", raising=False)
        monkeypatch.setenv("LOOM_ENV", "production")
        assert is_production() is True

    def test_is_production_false_locally(self, monkeypatch):
        """No cloud env vars -> local mode."""
        monkeypatch.delenv("K_SERVICE", raising=False)
        monkeypatch.delenv("LOOM_ENV", raising=False)
        assert is_production() is False

    def test_setup_logging_runs(self):
        """setup_logging() configures handlers without error."""
        setup_logging("DEBUG")
        root = logging.getLogger()
        assert len(root.handlers) >= 1


# ---------------------------------------------------------------------------
# Health check tests
# ---------------------------------------------------------------------------


class TestHealthCheck:
    async def test_health_check_200(self, pool, redis_conn):
        """Health endpoint returns 200 with healthy JSON."""
        from loom.mcp.server import _health_handler

        # Simulate a health check via the handler
        reader = asyncio.StreamReader()
        reader.feed_data(b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n")
        reader.feed_eof()

        # Capture writer output
        transport = _MockTransport()
        writer = asyncio.StreamWriter(
            transport, asyncio.Protocol(), reader, asyncio.get_event_loop()
        )

        await _health_handler(reader, writer, pool, redis_conn)

        output = transport.data.decode()
        assert "200 OK" in output
        body_start = output.index("{")
        body = json.loads(output[body_start:])
        assert body["status"] == "healthy"
        assert body["postgres"] == "ok"
        assert body["redis"] == "ok"

    async def test_health_check_responds_to_request(self, pool, redis_conn):
        """HTTP request/response cycle works end-to-end via asyncio server."""
        from loom.mcp.server import _health_handler

        server = await asyncio.start_server(
            lambda r, w: _health_handler(r, w, pool, redis_conn),
            host="127.0.0.1",
            port=0,  # OS picks a free port
        )
        addr = server.sockets[0].getsockname()
        port = addr[1]

        try:
            reader, writer = await asyncio.open_connection("127.0.0.1", port)
            writer.write(b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n")
            await writer.drain()

            response = await asyncio.wait_for(reader.read(4096), timeout=5.0)
            response_str = response.decode()

            assert "200 OK" in response_str
            body_start = response_str.index("{")
            body = json.loads(response_str[body_start:])
            assert body["status"] == "healthy"

            writer.close()
        finally:
            server.close()
            await server.wait_closed()


class _MockTransport:
    """Minimal transport that captures written bytes."""

    def __init__(self):
        self.data = b""
        self._closing = False

    def write(self, data: bytes):
        self.data += data

    def is_closing(self):
        return self._closing

    def close(self):
        self._closing = True

    def get_extra_info(self, name, default=None):
        return default
