"""Integration tests for Queries against a real PostgreSQL instance.

These tests require Docker.  They are automatically skipped when Docker
is not available (e.g. local dev without Docker Desktop running).
"""

from __future__ import annotations

import shutil
import subprocess
from datetime import datetime, timezone

import asyncpg  # type: ignore[import-untyped]
import pytest

from codeflash_api.db._queries import Queries

_DOCKER_AVAILABLE: bool | None = None


def _is_docker_available() -> bool:
    global _DOCKER_AVAILABLE  # noqa: PLW0603
    if _DOCKER_AVAILABLE is None:
        docker = shutil.which("docker")
        if docker is None:
            _DOCKER_AVAILABLE = False
        else:
            try:
                subprocess.run(  # noqa: S603
                    [docker, "info"],
                    capture_output=True,
                    timeout=5,
                    check=True,
                )
                _DOCKER_AVAILABLE = True
            except (
                subprocess.CalledProcessError,
                FileNotFoundError,
                OSError,
            ):
                _DOCKER_AVAILABLE = False
    return _DOCKER_AVAILABLE


pytestmark = [
    pytest.mark.skipif(
        not _is_docker_available(),
        reason="Docker is not available",
    ),
    pytest.mark.asyncio(),
]

SCHEMA_SQL = """\
CREATE TABLE IF NOT EXISTS cf_api_keys (
    id SERIAL PRIMARY KEY,
    key TEXT NOT NULL,
    suffix TEXT NOT NULL DEFAULT '',
    name TEXT NOT NULL DEFAULT '',
    user_id TEXT,
    tier TEXT,
    organization_id TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_used TIMESTAMPTZ
);

CREATE TABLE IF NOT EXISTS organizations (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    github_org_id TEXT,
    description TEXT,
    website TEXT,
    auto_add_github_members BOOLEAN NOT NULL DEFAULT TRUE,
    added_by TEXT,
    privacy_mode BOOLEAN NOT NULL DEFAULT FALSE,
    subscription BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS subscriptions (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL UNIQUE,
    plan_type TEXT NOT NULL,
    optimizations_used INT NOT NULL DEFAULT 0,
    total_lifetime_optimizations INT NOT NULL DEFAULT 0,
    optimizations_limit INT NOT NULL DEFAULT 4000,
    subscription_status TEXT NOT NULL DEFAULT 'active',
    stripe_customer_id TEXT,
    stripe_subscription_id TEXT,
    current_period_start TIMESTAMPTZ,
    current_period_end TIMESTAMPTZ,
    cancel_at_period_end BOOLEAN NOT NULL DEFAULT FALSE,
    cancellation_request_date TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);
"""


@pytest.fixture(scope="module")
def pg_container():
    """
    Start a PostgreSQL container for the test module.
    """
    from testcontainers.postgres import PostgresContainer  # type: ignore[import-untyped]  # noqa: I001

    with PostgresContainer("postgres:16-alpine") as pg:
        yield pg


@pytest.fixture(scope="module")
def pg_dsn(pg_container) -> str:  # type: ignore[no-untyped-def]
    """
    Connection string for the test Postgres instance.
    """
    return pg_container.get_connection_url().replace(
        "postgresql+psycopg2://", "postgresql://"
    )


@pytest.fixture
async def pool(pg_dsn: str) -> asyncpg.Pool:
    """
    Create a fresh pool and apply the schema for each test.
    """
    p = await asyncpg.create_pool(pg_dsn, min_size=1, max_size=5)
    assert p is not None
    async with p.acquire() as conn:
        await conn.execute(SCHEMA_SQL)
        await conn.execute("DELETE FROM subscriptions")
        await conn.execute("DELETE FROM cf_api_keys")
        await conn.execute("DELETE FROM organizations")
    yield p
    await p.close()


@pytest.fixture
def queries(pool: asyncpg.Pool) -> Queries:
    """
    Queries instance backed by the test pool.
    """
    return Queries(pool=pool)


async def _insert_api_key(
    pool: asyncpg.Pool,
    *,
    key: str = "hashed_abc",
    user_id: str = "user-1",
    suffix: str = "xabc",
    name: str = "Test Key",
) -> int:
    """
    Insert a test API key row and return its id.
    """
    row = await pool.fetchrow(
        "INSERT INTO cf_api_keys"
        " (key, suffix, name, user_id)"
        " VALUES ($1, $2, $3, $4)"
        " RETURNING id",
        key,
        suffix,
        name,
        user_id,
    )
    assert row is not None
    return int(row["id"])


class TestGetApiKeyByHash:
    """Tests for Queries.get_api_key_by_hash against real Postgres."""

    async def test_returns_matching_key(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        A key inserted with a known hash is found by that hash.
        """
        await _insert_api_key(pool, key="sha384_hash_abc")

        result = await queries.get_api_key_by_hash("sha384_hash_abc")

        assert result is not None
        assert "user-1" == result.user_id
        assert "xabc" == result.suffix

    async def test_returns_none_for_missing_hash(
        self,
        queries: Queries,
    ) -> None:
        """
        A hash with no matching row returns None.
        """
        result = await queries.get_api_key_by_hash("nonexistent_hash")

        assert result is None

    async def test_returns_correct_fields(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        All APIKey fields are populated from the database row.
        """
        await pool.execute(
            "INSERT INTO cf_api_keys"
            " (key, suffix, name, user_id, tier, organization_id)"
            " VALUES ($1, $2, $3, $4, $5, $6)",
            "full_fields_hash",
            "zz99",
            "Full Key",
            "user-42",
            "PRO",
            "org-7",
        )

        result = await queries.get_api_key_by_hash("full_fields_hash")

        assert result is not None
        assert "full_fields_hash" == result.key
        assert "zz99" == result.suffix
        assert "Full Key" == result.name
        assert "user-42" == result.user_id
        assert "PRO" == result.tier
        assert "org-7" == result.organization_id
        assert isinstance(result.created_at, datetime)


class TestUpdateApiKeyLastUsed:
    """Tests for Queries.update_api_key_last_used against real Postgres."""

    async def test_updates_timestamp(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        Calling update sets last_used to a recent timestamp.
        """
        key_id = await _insert_api_key(pool, key="update_test_hash")

        before = await pool.fetchval(
            "SELECT last_used FROM cf_api_keys WHERE id = $1",
            key_id,
        )
        assert before is None

        await queries.update_api_key_last_used(key_id)

        after = await pool.fetchval(
            "SELECT last_used FROM cf_api_keys WHERE id = $1",
            key_id,
        )
        assert after is not None
        assert isinstance(after, datetime)


class TestGetOrganization:
    """Tests for Queries.get_organization against real Postgres."""

    async def test_returns_matching_org(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        An inserted organization is found by ID.
        """
        await pool.execute(
            "INSERT INTO organizations (id, name, privacy_mode)"
            " VALUES ($1, $2, $3)",
            "org-1",
            "Acme Corp",
            True,
        )

        result = await queries.get_organization("org-1")

        assert result is not None
        assert "Acme Corp" == result.name
        assert result.privacy_mode is True

    async def test_returns_none_for_missing_org(
        self,
        queries: Queries,
    ) -> None:
        """
        A missing organization ID returns None.
        """
        result = await queries.get_organization("nonexistent-org")

        assert result is None


class TestGetSubscription:
    """Tests for Queries.get_subscription against real Postgres."""

    async def test_returns_matching_subscription(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        An inserted subscription is found by user_id.
        """
        await pool.execute(
            "INSERT INTO subscriptions"
            " (id, user_id, plan_type, optimizations_limit)"
            " VALUES ($1, $2, $3, $4)",
            "sub-1",
            "user-1",
            "PRO",
            100_000,
        )

        result = await queries.get_subscription("user-1")

        assert result is not None
        assert "PRO" == result.plan_type
        assert 100_000 == result.optimizations_limit
        assert 0 == result.optimizations_used

    async def test_returns_none_for_missing_user(
        self,
        queries: Queries,
    ) -> None:
        """
        A user_id with no subscription returns None.
        """
        result = await queries.get_subscription("ghost-user")

        assert result is None


class TestCreateSubscription:
    """Tests for Queries.create_subscription against real Postgres."""

    async def test_creates_and_returns_subscription(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        A new subscription is persisted and returned with defaults.
        """
        result = await queries.create_subscription(
            user_id="new-user",
            plan_type="FREE",
            optimizations_limit=4000,
        )

        assert "new-user" == result.user_id
        assert "FREE" == result.plan_type
        assert 4000 == result.optimizations_limit
        assert 0 == result.optimizations_used

        row = await pool.fetchrow(
            "SELECT * FROM subscriptions WHERE user_id = $1",
            "new-user",
        )
        assert row is not None
        assert "active" == row["subscription_status"]

    async def test_string_limit_is_cast_to_int(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        A string optimizations_limit is cast to int before insert.
        """
        result = await queries.create_subscription(
            user_id="str-limit-user",
            plan_type="PRO",
            optimizations_limit="100000",
        )

        assert 100_000 == result.optimizations_limit

        db_val = await pool.fetchval(
            "SELECT optimizations_limit FROM subscriptions WHERE user_id = $1",
            "str-limit-user",
        )
        assert 100_000 == db_val


class TestIncrementUsage:
    """Tests for Queries.increment_usage against real Postgres."""

    async def test_increments_both_counters(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        Both optimizations_used and total_lifetime_optimizations increase.
        """
        await pool.execute(
            "INSERT INTO subscriptions"
            " (id, user_id, plan_type)"
            " VALUES ($1, $2, $3)",
            "sub-inc",
            "inc-user",
            "FREE",
        )

        await queries.increment_usage("inc-user", cost=5)

        row = await pool.fetchrow(
            "SELECT optimizations_used, total_lifetime_optimizations"
            " FROM subscriptions WHERE user_id = $1",
            "inc-user",
        )
        assert row is not None
        assert 5 == row["optimizations_used"]
        assert 5 == row["total_lifetime_optimizations"]

    async def test_increments_are_cumulative(
        self,
        queries: Queries,
        pool: asyncpg.Pool,
    ) -> None:
        """
        Multiple increments accumulate correctly.
        """
        await pool.execute(
            "INSERT INTO subscriptions"
            " (id, user_id, plan_type)"
            " VALUES ($1, $2, $3)",
            "sub-cum",
            "cum-user",
            "FREE",
        )

        await queries.increment_usage("cum-user", cost=3)
        await queries.increment_usage("cum-user", cost=7)

        row = await pool.fetchrow(
            "SELECT optimizations_used, total_lifetime_optimizations"
            " FROM subscriptions WHERE user_id = $1",
            "cum-user",
        )
        assert row is not None
        assert 10 == row["optimizations_used"]
        assert 10 == row["total_lifetime_optimizations"]
