#
# This file runs pg_regress-based tests.
#
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, cast

import pytest
from fixtures.neon_fixtures import (
    Endpoint,
    NeonEnv,
    NeonEnvBuilder,
    check_restored_datadir_content,
    tenant_get_shards,
)
from fixtures.pg_version import PgVersion
from fixtures.remote_storage import s3_storage

if TYPE_CHECKING:
    from typing import Optional

    from fixtures.neon_fixtures import PgBin
    from pytest import CaptureFixture


TENANT_CONF = {
    # Scaled down thresholds so that we are exercising the pageserver beyond just writing
    # ephemeral/L0 layers, and because debug-mode code is slow to read from full sized ephemeral layer files.
    "pitr_interval": "60s",
    "checkpoint_distance": f"{8 * 1024 * 1024}",
    "compaction_target_size": f"{8 * 1024 * 1024}",
}

# # Ensure that compaction works, on a timeline containing all the diversity that postgres regression tests create.
# # There should have been compactions mid-test as well, this final check is in addition those.
# for (shard, pageserver) in tenant_get_shards(env, env.initial_tenant):
#     pageserver.http_client().timeline_checkpoint(env.initial_tenant, env.initial_timeline, force_repartition=True, force_image_layer_creation=True)


def post_checks(env: NeonEnv, test_output_dir: Path, db_name: str, endpoint: Endpoint):
    """
    After running some opaque tests that create interesting content in a timeline, run
    some generic integrity checks that the storage stack is able to reproduce the written
    data properly.
    """

    ignored_files: Optional[list[str]] = None

    # Neon handles unlogged relations in a special manner. During a
    # basebackup, we ship the init fork as the main fork. This presents a
    # problem in that the endpoint's data directory and the basebackup will
    # have differences and will fail the eventual file comparison.
    #
    # Unlogged tables were introduced in version 9.1. ALTER TABLE grew
    # support for setting the persistence of a table in 9.5. The reason that
    # this doesn't affect versions < 15 (but probably would between 9.1 and
    # 9.5) is that all the regression tests that deal with unlogged tables
    # up until that point dropped the unlogged tables or set them to logged
    # at some point during the test.
    #
    # In version 15, Postgres grew support for unlogged sequences, and with
    # that came a few more regression tests. These tests did not all drop
    # the unlogged tables/sequences prior to finishing.
    #
    # But unlogged sequences came with a bug in that, sequences didn't
    # inherit the persistence of their "parent" tables if they had one. This
    # was fixed and backported to 15, thus exacerbating our problem a bit.
    #
    # So what we can do is just ignore file differences between the data
    # directory and basebackup for unlogged relations.
    results = cast(
        "list[tuple[str, str]]",
        endpoint.safe_psql(
            """
        SELECT
            relkind,
            pg_relation_filepath(
                pg_filenode_relation(reltablespace, relfilenode)
            ) AS unlogged_relation_paths
        FROM pg_class
        WHERE relpersistence = 'u'
        """,
            dbname=db_name,
        ),
    )

    unlogged_relation_files: list[str] = []
    for r in results:
        unlogged_relation_files.append(r[1])
        # This is related to the following Postgres commit:
        #
        # commit ccadf73163ca88bdaa74b8223d4dde05d17f550b
        # Author: Heikki Linnakangas <heikki.linnakangas@iki.fi>
        # Date:   2023-08-23 09:21:31 -0500
        #
        # Use the buffer cache when initializing an unlogged index.
        #
        # This patch was backpatched to 16. Without it, the LSN in the
        # page header would be 0/0 in the data directory, which wouldn't
        # match the LSN generated during the basebackup, thus creating
        # a difference.
        if env.pg_version <= PgVersion.V15 and r[0] == "i":
            unlogged_relation_files.append(f"{r[1]}_init")

    ignored_files = unlogged_relation_files

    check_restored_datadir_content(test_output_dir, env, endpoint, ignored_files=ignored_files)

    # Ensure that compaction works, on a timeline containing all the diversity that postgres regression tests create.
    # There should have been compactions mid-test as well, this final check is in addition those.
    for shard, pageserver in tenant_get_shards(env, env.initial_tenant):
        pageserver.http_client().timeline_checkpoint(
            shard, env.initial_timeline, force_repartition=True, force_image_layer_creation=True
        )


# Run the main PostgreSQL regression tests, in src/test/regress.
#
@pytest.mark.timeout(900)  # Contains many sub-tests, is slow in debug builds
@pytest.mark.parametrize("shard_count", [None, 4])
def test_pg_regress(
    neon_env_builder: NeonEnvBuilder,
    test_output_dir: Path,
    pg_bin: PgBin,
    capsys: CaptureFixture[str],
    base_dir: Path,
    pg_distrib_dir: Path,
    shard_count: Optional[int],
):
    DBNAME = "regression"

    """
    :param shard_count: if None, create an unsharded tenant.  Otherwise create a tenant with this
                        many shards.
    """
    if shard_count is not None:
        neon_env_builder.num_pageservers = shard_count

    neon_env_builder.enable_pageserver_remote_storage(s3_storage())
    env = neon_env_builder.init_start(
        initial_tenant_conf=TENANT_CONF,
        initial_tenant_shard_count=shard_count,
    )

    # Connect to postgres and create a database called "regression".
    endpoint = env.endpoints.create_start(
        "main",
        config_lines=[
            # Enable the test mode, so that we don't need to patch the test cases.
            "neon.regress_test_mode = true",
        ],
    )
    endpoint.safe_psql(f"CREATE DATABASE {DBNAME}")

    # Create some local directories for pg_regress to run in.
    runpath = test_output_dir / "regress"
    (runpath / "testtablespace").mkdir(parents=True)

    # Compute all the file locations that pg_regress will need.
    build_path = pg_distrib_dir / f"build/{env.pg_version.v_prefixed}/src/test/regress"
    src_path = base_dir / f"vendor/postgres-{env.pg_version.v_prefixed}/src/test/regress"
    bindir = pg_distrib_dir / f"v{env.pg_version}/bin"
    schedule = src_path / "parallel_schedule"
    pg_regress = build_path / "pg_regress"

    pg_regress_command = [
        str(pg_regress),
        '--bindir=""',
        "--use-existing",
        f"--bindir={bindir}",
        f"--dlpath={build_path}",
        f"--schedule={schedule}",
        f"--inputdir={src_path}",
    ]

    env_vars = {
        "PGPORT": str(endpoint.default_options["port"]),
        "PGUSER": endpoint.default_options["user"],
        "PGHOST": endpoint.default_options["host"],
    }

    # Run the command.
    # We don't capture the output. It's not too chatty, and it always
    # logs the exact same data to `regression.out` anyway.
    with capsys.disabled():
        pg_bin.run(pg_regress_command, env=env_vars, cwd=runpath)

    post_checks(env, test_output_dir, DBNAME, endpoint)


# Run the PostgreSQL "isolation" tests, in src/test/isolation.
#
@pytest.mark.timeout(600)  # Contains many sub-tests, is slow in debug builds
@pytest.mark.parametrize("shard_count", [None, 4])
def test_isolation(
    neon_env_builder: NeonEnvBuilder,
    test_output_dir: Path,
    pg_bin: PgBin,
    capsys: CaptureFixture[str],
    base_dir: Path,
    pg_distrib_dir: Path,
    shard_count: Optional[int],
):
    DBNAME = "isolation_regression"

    if shard_count is not None:
        neon_env_builder.num_pageservers = shard_count
    neon_env_builder.enable_pageserver_remote_storage(s3_storage())
    env = neon_env_builder.init_start(
        initial_tenant_conf=TENANT_CONF, initial_tenant_shard_count=shard_count
    )

    # Connect to postgres and create a database called "regression".
    # isolation tests use prepared transactions, so enable them
    endpoint = env.endpoints.create_start(
        "main",
        config_lines=[
            "max_prepared_transactions=100",
            # Enable the test mode, so that we don't need to patch the test cases.
            "neon.regress_test_mode = true",
        ],
    )
    endpoint.safe_psql(f"CREATE DATABASE {DBNAME}")

    # Create some local directories for pg_isolation_regress to run in.
    runpath = test_output_dir / "regress"
    (runpath / "testtablespace").mkdir(parents=True)

    # Compute all the file locations that pg_isolation_regress will need.
    build_path = pg_distrib_dir / f"build/{env.pg_version.v_prefixed}/src/test/isolation"
    src_path = base_dir / f"vendor/postgres-{env.pg_version.v_prefixed}/src/test/isolation"
    bindir = pg_distrib_dir / f"v{env.pg_version}/bin"
    schedule = src_path / "isolation_schedule"
    pg_isolation_regress = build_path / "pg_isolation_regress"

    pg_isolation_regress_command = [
        str(pg_isolation_regress),
        "--use-existing",
        f"--bindir={bindir}",
        f"--dlpath={build_path}",
        f"--inputdir={src_path}",
        f"--schedule={schedule}",
    ]

    env_vars = {
        "PGPORT": str(endpoint.default_options["port"]),
        "PGUSER": endpoint.default_options["user"],
        "PGHOST": endpoint.default_options["host"],
    }

    # Run the command.
    # We don't capture the output. It's not too chatty, and it always
    # logs the exact same data to `regression.out` anyway.
    with capsys.disabled():
        pg_bin.run(pg_isolation_regress_command, env=env_vars, cwd=runpath)

    # This fails with a mismatch on `pg_multixact/offsets/0000`
    # post_checks(env, test_output_dir, DBNAME, endpoint)


# Run extra Neon-specific pg_regress-based tests. The tests and their
# schedule file are in the sql_regress/ directory.
@pytest.mark.parametrize("shard_count", [None, 4])
def test_sql_regress(
    neon_env_builder: NeonEnvBuilder,
    test_output_dir: Path,
    pg_bin: PgBin,
    capsys: CaptureFixture[str],
    base_dir: Path,
    pg_distrib_dir: Path,
    shard_count: Optional[int],
):
    DBNAME = "regression"

    if shard_count is not None:
        neon_env_builder.num_pageservers = shard_count
    neon_env_builder.enable_pageserver_remote_storage(s3_storage())
    env = neon_env_builder.init_start(
        initial_tenant_conf=TENANT_CONF, initial_tenant_shard_count=shard_count
    )

    # Connect to postgres and create a database called "regression".
    endpoint = env.endpoints.create_start(
        "main",
        config_lines=[
            # Enable the test mode, so that we don't need to patch the test cases.
            "neon.regress_test_mode = true",
        ],
    )
    endpoint.safe_psql(f"CREATE DATABASE {DBNAME}")

    # Create some local directories for pg_regress to run in.
    runpath = test_output_dir / "regress"
    (runpath / "testtablespace").mkdir(parents=True)

    # Compute all the file locations that pg_regress will need.
    # This test runs neon specific tests
    build_path = pg_distrib_dir / f"build/v{env.pg_version}/src/test/regress"
    src_path = base_dir / "test_runner/sql_regress"
    bindir = pg_distrib_dir / f"v{env.pg_version}/bin"
    schedule = src_path / "parallel_schedule"
    pg_regress = build_path / "pg_regress"

    pg_regress_command = [
        str(pg_regress),
        "--use-existing",
        f"--bindir={bindir}",
        f"--dlpath={build_path}",
        f"--schedule={schedule}",
        f"--inputdir={src_path}",
    ]

    env_vars = {
        "PGPORT": str(endpoint.default_options["port"]),
        "PGUSER": endpoint.default_options["user"],
        "PGHOST": endpoint.default_options["host"],
    }

    # Run the command.
    # We don't capture the output. It's not too chatty, and it always
    # logs the exact same data to `regression.out` anyway.
    with capsys.disabled():
        pg_bin.run(pg_regress_command, env=env_vars, cwd=runpath)

    post_checks(env, test_output_dir, DBNAME, endpoint)
