from __future__ import annotations

import json
import shutil
import subprocess
import sys
import time
import uuid
from pathlib import Path

import pytest

from ops import run_local_canonical_postgres_rebuild as rebuild


def _docker_available() -> bool:
    if shutil.which("docker") is None:
        return False
    completed = subprocess.run(
        ["docker", "info", "--format", "{{.ServerVersion}}"],
        text=True,
        capture_output=True,
        check=False,
    )
    return completed.returncode == 0


def _docker_psql(container: str, database: str, sql: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [
            "docker",
            "exec",
            "-i",
            container,
            "psql",
            "-X",
            "-U",
            "postgres",
            "-d",
            database,
            "-Atq",
            "-v",
            "ON_ERROR_STOP=1",
        ],
        input=sql,
        text=True,
        capture_output=True,
        check=False,
    )


def _wait_for_postgres(container: str, database: str) -> None:
    for _ in range(120):
        completed = _docker_psql(container, database, "select 1;")
        if completed.returncode == 0:
            return
        time.sleep(0.25)
    pytest.fail(f"fixture PostgreSQL did not become ready: {container}")


def _require_success(completed: subprocess.CompletedProcess[str], action: str) -> None:
    if completed.returncode != 0:
        pytest.fail(
            f"{action} failed ({completed.returncode})\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
        )


def _capture_manifest(container: str, database: str, destination: Path) -> None:
    copy = subprocess.run(
        ["docker", "cp", str(rebuild.DEFAULT_MANIFEST_SQL), f"{container}:/tmp/parity.sql"],
        text=True,
        capture_output=True,
        check=False,
    )
    _require_success(copy, "manifest SQL copy")
    capture = subprocess.run(
        [
            "docker",
            "exec",
            container,
            "psql",
            "-X",
            "-U",
            "postgres",
            "-d",
            database,
            "-Atq",
            "-v",
            "ON_ERROR_STOP=1",
            "-f",
            "/tmp/parity.sql",
        ],
        text=True,
        capture_output=True,
        check=False,
    )
    _require_success(capture, "source manifest capture")
    destination.write_text(capture.stdout, encoding="utf-8")


def _capture_dump(container: str, database: str, destination: Path) -> None:
    dump = subprocess.run(
        [
            "docker",
            "exec",
            container,
            "pg_dump",
            "-U",
            "postgres",
            "-d",
            database,
            "--format=custom",
            "--file=/tmp/canonical.dump",
        ],
        text=True,
        capture_output=True,
        check=False,
    )
    _require_success(dump, "fixture pg_dump")
    copy = subprocess.run(
        ["docker", "cp", f"{container}:/tmp/canonical.dump", str(destination)],
        text=True,
        capture_output=True,
        check=False,
    )
    _require_success(copy, "fixture dump copy")


def _fixture_sql(database: str) -> str:
    return f"""
create schema kb_stage;
create table public.claims (id bigint primary key, body text not null);
create table public.sources (id bigint primary key, url text not null);
create table public.claim_evidence (
  id bigint primary key,
  claim_id bigint not null references public.claims(id),
  source_id bigint not null references public.sources(id)
);
create table public.claim_edges (
  id bigint primary key,
  from_claim bigint not null references public.claims(id),
  to_claim bigint not null references public.claims(id)
);
create table public.reasoning_tools (id bigint primary key, name text not null);
create table kb_stage.kb_proposals (
  id bigint primary key,
  status text not null,
  created_at timestamptz not null
);
insert into public.claims values (1, 'fixture claim');
insert into public.sources values (1, 'https://example.invalid/source');
insert into public.claim_evidence values (1, 1, 1);
insert into public.claim_edges values (1, 1, 1);
insert into public.reasoning_tools values (1, 'fixture tool');
insert into kb_stage.kb_proposals values (1, 'pending_review', '2026-07-16T00:00:00Z');

create role kb_review nologin noinherit;
create role kb_apply login inherit;
create role kb_observatory_read nologin noinherit;
alter role kb_apply set application_name to 'teleo=parity-fixture';
alter role kb_apply in database "{database}" set statement_timeout to '17s';
alter role kb_observatory_read in database "{database}" set default_transaction_read_only to 'on';

grant kb_review to kb_apply
  with admin true, inherit false, set false
  granted by postgres;
set role kb_apply;
grant kb_review to kb_observatory_read
  with admin false, inherit true, set false
  granted by kb_apply;
reset role;

revoke all privileges on database "{database}" from public cascade;
grant connect on database "{database}" to public granted by postgres;
grant connect on database "{database}" to kb_review with grant option granted by postgres;
set role kb_review;
grant connect on database "{database}" to kb_apply granted by kb_review;
reset role;
"""


@pytest.mark.skipif(not _docker_available(), reason="Docker daemon is required for PostgreSQL parity")
def test_real_postgres_rebuild_replays_scoped_settings_memberships_public_acl_and_grantors(
    tmp_path: Path,
) -> None:
    source_container = f"teleo-pr173-source-{uuid.uuid4().hex[:12]}"
    source_database = "teleo_source"
    started = subprocess.run(
        [
            "docker",
            "run",
            "--rm",
            "--detach",
            "--network",
            "none",
            "--name",
            source_container,
            "--tmpfs",
            f"{rebuild.DATA_DIR}:rw,nosuid,nodev,noexec,size=256m",
            "--env",
            f"POSTGRES_DB={source_database}",
            "--env",
            "POSTGRES_HOST_AUTH_METHOD=trust",
            rebuild.DEFAULT_IMAGE,
        ],
        text=True,
        capture_output=True,
        check=False,
    )
    _require_success(started, "source PostgreSQL start")

    cleanup_verified = False
    try:
        _wait_for_postgres(source_container, source_database)
        bootstrap = _docker_psql(source_container, source_database, _fixture_sql(source_database))
        _require_success(bootstrap, "source parity fixture bootstrap")

        source_manifest_path = tmp_path / "source-manifest.jsonl"
        source_dump_path = tmp_path / "source.dump"
        _capture_manifest(source_container, source_database, source_manifest_path)
        _capture_dump(source_container, source_database, source_dump_path)

        source_manifest = rebuild.load_manifest(source_manifest_path)
        roles = {row["name"]: row for row in source_manifest["singleton"]["application_roles"]["items"]}
        assert roles["kb_apply"]["settings"] == [{"name": "application_name", "value": "teleo=parity-fixture"}]
        assert roles["kb_apply"]["database_settings"] == [{"name": "statement_timeout", "value": "17s"}]
        assert roles["kb_observatory_read"]["database_settings"] == [
            {"name": "default_transaction_read_only", "value": "on"}
        ]
        memberships = source_manifest["singleton"]["role_memberships"]["items"]
        assert {row["grantor"] for row in memberships} == {"postgres", "kb_apply"}
        assert any(
            row["admin_option"] is True and row["inherit_option"] is False and row["set_option"] is False
            for row in memberships
        )
        database_acls = [
            row for row in source_manifest["singleton"]["object_acl"]["items"] if row["object_type"] == "database"
        ]
        assert any(row["grantee"] == "PUBLIC" and row["privilege_type"] == "CONNECT" for row in database_acls)
        assert not any(row["grantee"] == "PUBLIC" and row["privilege_type"] == "TEMPORARY" for row in database_acls)
        assert any(row["grantor"] == "kb_review" and row["grantee"] == "kb_apply" for row in database_acls)

        receipt_path = tmp_path / "parity-receipt.json"
        completed = subprocess.run(
            [
                sys.executable,
                "ops/run_local_canonical_postgres_rebuild.py",
                "--dump",
                str(source_dump_path),
                "--database",
                "teleo_target",
                "--source-manifest",
                str(source_manifest_path),
                "--manifest-sql",
                str(rebuild.DEFAULT_MANIFEST_SQL),
                "--tmpfs-mb",
                "256",
                "--max-target-query-ms",
                "10000",
                "--max-target-source-ratio",
                "1000",
                "--output",
                str(receipt_path),
            ],
            text=True,
            capture_output=True,
            check=False,
        )
        receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
        assert completed.returncode == 0, completed.stdout + completed.stderr
        assert receipt["status"] == "pass", receipt.get("error")
        assert receipt["application_roles_bootstrapped"] == [
            "kb_apply",
            "kb_observatory_read",
            "kb_review",
        ]
        assert receipt["role_setting_statement_count"] == 1
        assert receipt["database_role_setting_statement_count"] == 2
        assert receipt["role_membership_statement_count"] == 2
        assert receipt["database_acl_statement_count"] == len(database_acls)
        assert receipt["parity"]["status"] == "pass"
        assert receipt["parity"]["problems"] == []
        structural = receipt["parity"]["details"]["structural_hashes"]
        assert structural["role_memberships"]["source"] == structural["role_memberships"]["target"]
        assert structural["object_acl"]["source"] == structural["object_acl"]["target"]
        application_roles = receipt["parity"]["details"]["application_role_hashes"]
        assert application_roles["source"] == application_roles["target"]
        assert receipt["parity"]["details"]["application_role_mismatches"] == {}
        assert receipt["parity"]["source_manifest"]["sha256"] != receipt["parity"]["target_manifest"]["sha256"]
        assert receipt["cleanup"]["status"] == "pass"
        assert receipt["cleanup"]["container_absent"] is True
    finally:
        subprocess.run(
            ["docker", "rm", "--force", source_container],
            text=True,
            capture_output=True,
            check=False,
        )
        inspected = subprocess.run(
            ["docker", "container", "inspect", source_container],
            text=True,
            capture_output=True,
            check=False,
        )
        cleanup_verified = inspected.returncode != 0

    assert cleanup_verified is True
