import os
from pathlib import Path

# Deterministic unit-test config — MUST be set before any app import
# (app.config snapshots env at import; app.db builds the engine from it).
# BW_AUTH points at an unroutable host so an accidental real network call
# fails fast instead of hitting production id-auth. The secret is a
# throwaway test string, never a real credential.
os.environ["APP_ENV"] = "test"
os.environ["BW_CLIENT_ID"] = "voll-test"
os.environ["BW_CLIENT_SECRET"] = "unit-test-secret-not-a-real-one"
os.environ["BW_APP_DOMAIN"] = "https://testserver"
os.environ["BW_AUTH"] = "https://bw-auth.invalid"
os.environ.pop("AUTH_DEV_USER", None)
os.environ.pop("SESSION_SECRET", None)

# DB-touching tests use the LOCAL dev Postgres (127.0.0.1:5433) but a
# SEPARATE database — never the in-use `volleyboard` database.
TEST_DB_NAME = "volleyboard_test"
PG_HOST, PG_PORT, PG_USER, PG_PASSWORD = "127.0.0.1", 5433, "volleyboard", "volleyboard"
os.environ["DATABASE_URL"] = (
    f"postgresql+psycopg2://{PG_USER}:{PG_PASSWORD}@{PG_HOST}:{PG_PORT}/{TEST_DB_NAME}"
)

import psycopg2  # noqa: E402
import pytest  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402
from sqlalchemy import text  # noqa: E402

from app import db as app_db  # noqa: E402
from app.main import create_app  # noqa: E402
from app.models import Match, Player, Side, TeamNode  # noqa: E402
from app.services.auth import session as auth_session  # noqa: E402

BACKEND_DIR = Path(__file__).resolve().parents[1]

_ALL_TABLES = (
    "touches",
    "balls",
    "team_members",
    "team_nodes",
    "courts",
    "sides",
    "matches",
    "players",
)


def _admin_conn():
    """Autocommit connection to the cluster's maintenance DB (never the
    in-use `volleyboard` database) for CREATE/DROP DATABASE."""
    conn = psycopg2.connect(
        host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname="postgres"
    )
    conn.autocommit = True
    return conn


@pytest.fixture(scope="session", autouse=True)
def test_database():
    """Create volleyboard_test, migrate it to head, drop it at the end."""
    conn = _admin_conn()
    with conn.cursor() as cur:
        cur.execute(f"DROP DATABASE IF EXISTS {TEST_DB_NAME} WITH (FORCE)")
        cur.execute(f"CREATE DATABASE {TEST_DB_NAME}")
    conn.close()

    from alembic import command
    from alembic.config import Config

    cfg = Config(str(BACKEND_DIR / "alembic.ini"))
    cfg.set_main_option("script_location", str(BACKEND_DIR / "alembic"))
    command.upgrade(cfg, "head")

    yield

    app_db.engine.dispose()
    conn = _admin_conn()
    with conn.cursor() as cur:
        cur.execute(f"DROP DATABASE IF EXISTS {TEST_DB_NAME} WITH (FORCE)")
    conn.close()


@pytest.fixture(autouse=True)
def _clean_db(test_database):
    """Every test starts from empty tables."""
    with app_db.engine.begin() as conn:
        conn.execute(text(f"TRUNCATE {', '.join(_ALL_TABLES)} CASCADE"))
    yield


@pytest.fixture()
def db_session():
    session = app_db.SessionLocal()
    yield session
    session.close()


@pytest.fixture()
def client():
    # https base_url: the app's cookies are Secure, and httpx's jar
    # (correctly) refuses to send Secure cookies over plain http.
    with TestClient(create_app(), base_url="https://testserver") as test_client:
        yield test_client


@pytest.fixture()
def authed_client(client):
    """Session-mint shortcut: a client holding a valid signed app-session
    cookie for 'rian' (no OAuth round-trip) — for endpoint tests."""
    token = auth_session.mint_session(username="rian", email="rian@rian.ca")
    client.cookies.set(auth_session.SESSION_COOKIE, token)
    return client


def mint_client_for(client, username, email=""):
    """Re-point a client's session cookie at another username."""
    token = auth_session.mint_session(username=username, email=email)
    client.cookies.set(auth_session.SESSION_COOKIE, token)
    return client


# ------------------------------------------------------------- row factories


def make_player(db, username, display_name=None, email=None) -> Player:
    player = Player(username=username, display_name=display_name, email=email)
    db.add(player)
    db.commit()
    return player


def make_lead(db, player: Player, match_slug: str | None = None) -> TeamNode:
    """Make `player` a lead: boundary player of a team node."""
    match = Match(slug=match_slug or f"m-{player.username}", name="Test Match")
    db.add(match)
    db.flush()
    side = Side(match_id=match.id, name="Side A", position=0)
    db.add(side)
    db.flush()
    node = TeamNode(side_id=side.id, name="Front line", boundary_player_id=player.id)
    db.add(node)
    db.commit()
    return node


def make_fleshout_match(db, slug: str = "fleshout"):
    """The plan §4.1 worked example — THE perspective test roster (mirrors
    tools/load-seed.py's build_team_tree):

        SIDE Bowden Works: Front line (boundary rian): rian, adi
                           -> Tingang (boundary adi): zeina, rahman, stevan
        SIDE Brentwood:    Front line (boundary erin): erin, tracy
                           -> Back office (boundary erin): rob

    Returns {"match", "players" (by username), "nodes" (by key), "court"
    (an issues court, ready for rally/board tests)}.
    """
    from sqlalchemy import select

    from app import constants
    from app.models import Court, TeamMember

    players = {}
    for username in ("rian", "adi", "zeina", "rahman", "stevan", "erin", "tracy", "rob"):
        existing = db.execute(
            select(Player).where(Player.username == username)
        ).scalar_one_or_none()
        players[username] = existing or make_player(
            db, username, display_name=username.capitalize()
        )

    match = Match(slug=slug, name="Fleshout Match")
    db.add(match)
    db.flush()
    bowden = Side(match_id=match.id, name="Bowden Works", position=1)
    brentwood = Side(match_id=match.id, name="Brentwood", position=2)
    db.add_all([bowden, brentwood])
    db.flush()
    bowden_front = TeamNode(
        side_id=bowden.id, name="Front line", boundary_player_id=players["rian"].id
    )
    brentwood_front = TeamNode(
        side_id=brentwood.id, name="Front line", boundary_player_id=players["erin"].id
    )
    db.add_all([bowden_front, brentwood_front])
    db.flush()
    tingang = TeamNode(
        side_id=bowden.id,
        parent_node_id=bowden_front.id,
        name="Tingang",
        boundary_player_id=players["adi"].id,
    )
    back_office = TeamNode(
        side_id=brentwood.id,
        parent_node_id=brentwood_front.id,
        name="Back office",
        boundary_player_id=players["erin"].id,
    )
    db.add_all([tingang, back_office])
    db.flush()
    membership = [
        (bowden_front, ["rian", "adi"]),
        (tingang, ["zeina", "rahman", "stevan"]),
        (brentwood_front, ["erin", "tracy"]),
        (back_office, ["rob"]),
    ]
    for node, usernames in membership:
        for username in usernames:
            db.add(
                TeamMember(
                    match_id=match.id, node_id=node.id, player_id=players[username].id
                )
            )
    court = Court(
        match_id=match.id,
        config_key="issues",
        name="Issues",
        position=0,
        config=constants.COURT_CONFIGS["issues"],
    )
    db.add(court)
    db.commit()
    return {
        "match": match,
        "players": players,
        "nodes": {
            "bowden_front": bowden_front,
            "tingang": tingang,
            "brentwood_front": brentwood_front,
            "back_office": back_office,
        },
        "court": court,
    }
