from __future__ import annotations

import os

import pytest

HERE = os.path.dirname(os.path.abspath(__file__))


@pytest.mark.parametrize(
    "filename",
    ["adapters_example.py", "typing_example.py"],
)
def test_typing_example(mypy, filename):
    cp = mypy.run_on_file(os.path.join(HERE, filename))
    errors = cp.stdout.decode("utf8", "replace").splitlines()
    assert not errors
    assert cp.returncode == 0


@pytest.mark.parametrize(
    "conn, type",
    [
        (
            "psycopg.connect()",
            "psycopg.Connection[Tuple[Any, ...]]",
        ),
        (
            "psycopg.connect(row_factory=rows.tuple_row)",
            "psycopg.Connection[Tuple[Any, ...]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "psycopg.Connection[Dict[str, Any]]",
        ),
        (
            "psycopg.connect(row_factory=rows.namedtuple_row)",
            "psycopg.Connection[NamedTuple]",
        ),
        (
            "psycopg.connect(row_factory=rows.class_row(Thing))",
            "psycopg.Connection[Thing]",
        ),
        (
            "psycopg.connect(row_factory=thing_row)",
            "psycopg.Connection[Thing]",
        ),
        (
            "psycopg.Connection.connect()",
            "psycopg.Connection[Tuple[Any, ...]]",
        ),
        (
            "psycopg.Connection.connect(row_factory=rows.dict_row)",
            "psycopg.Connection[Dict[str, Any]]",
        ),
        (
            "await psycopg.AsyncConnection.connect()",
            "psycopg.AsyncConnection[Tuple[Any, ...]]",
        ),
        (
            "await psycopg.AsyncConnection.connect(row_factory=rows.dict_row)",
            "psycopg.AsyncConnection[Dict[str, Any]]",
        ),
    ],
)
def test_connection_type(conn, type, mypy):
    stmts = f"obj = {conn}"
    _test_reveal(stmts, type, mypy)


@pytest.mark.parametrize(
    "conn, curs, type",
    [
        (
            "psycopg.connect()",
            "conn.cursor()",
            "psycopg.Cursor[Tuple[Any, ...]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "conn.cursor()",
            "psycopg.Cursor[Dict[str, Any]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "conn.cursor(row_factory=rows.namedtuple_row)",
            "psycopg.Cursor[NamedTuple]",
        ),
        (
            "psycopg.connect(row_factory=rows.class_row(Thing))",
            "conn.cursor()",
            "psycopg.Cursor[Thing]",
        ),
        (
            "psycopg.connect(row_factory=thing_row)",
            "conn.cursor()",
            "psycopg.Cursor[Thing]",
        ),
        (
            "psycopg.connect()",
            "conn.cursor(row_factory=thing_row)",
            "psycopg.Cursor[Thing]",
        ),
        # Async cursors
        (
            "await psycopg.AsyncConnection.connect()",
            "conn.cursor()",
            "psycopg.AsyncCursor[Tuple[Any, ...]]",
        ),
        (
            "await psycopg.AsyncConnection.connect()",
            "conn.cursor(row_factory=thing_row)",
            "psycopg.AsyncCursor[Thing]",
        ),
        # Server-side cursors
        (
            "psycopg.connect()",
            "conn.cursor(name='foo')",
            "psycopg.ServerCursor[Tuple[Any, ...]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "conn.cursor(name='foo')",
            "psycopg.ServerCursor[Dict[str, Any]]",
        ),
        (
            "psycopg.connect()",
            "conn.cursor(name='foo', row_factory=rows.dict_row)",
            "psycopg.ServerCursor[Dict[str, Any]]",
        ),
        # Async server-side cursors
        (
            "await psycopg.AsyncConnection.connect()",
            "conn.cursor(name='foo')",
            "psycopg.AsyncServerCursor[Tuple[Any, ...]]",
        ),
        (
            "await psycopg.AsyncConnection.connect(row_factory=rows.dict_row)",
            "conn.cursor(name='foo')",
            "psycopg.AsyncServerCursor[Dict[str, Any]]",
        ),
        (
            "await psycopg.AsyncConnection.connect()",
            "conn.cursor(name='foo', row_factory=rows.dict_row)",
            "psycopg.AsyncServerCursor[Dict[str, Any]]",
        ),
    ],
)
def test_cursor_type(conn, curs, type, mypy):
    stmts = f"""\
conn = {conn}
obj = {curs}
"""
    _test_reveal(stmts, type, mypy)


@pytest.mark.parametrize(
    "conn, curs, type",
    [
        (
            "psycopg.connect()",
            "psycopg.Cursor(conn)",
            "psycopg.Cursor[Tuple[Any, ...]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "psycopg.Cursor(conn)",
            "psycopg.Cursor[Dict[str, Any]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "psycopg.Cursor(conn, row_factory=rows.namedtuple_row)",
            "psycopg.Cursor[NamedTuple]",
        ),
        # Async cursors
        (
            "await psycopg.AsyncConnection.connect()",
            "psycopg.AsyncCursor(conn)",
            "psycopg.AsyncCursor[Tuple[Any, ...]]",
        ),
        (
            "await psycopg.AsyncConnection.connect(row_factory=rows.dict_row)",
            "psycopg.AsyncCursor(conn)",
            "psycopg.AsyncCursor[Dict[str, Any]]",
        ),
        (
            "await psycopg.AsyncConnection.connect()",
            "psycopg.AsyncCursor(conn, row_factory=thing_row)",
            "psycopg.AsyncCursor[Thing]",
        ),
        # Server-side cursors
        (
            "psycopg.connect()",
            "psycopg.ServerCursor(conn, 'foo')",
            "psycopg.ServerCursor[Tuple[Any, ...]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "psycopg.ServerCursor(conn, name='foo')",
            "psycopg.ServerCursor[Dict[str, Any]]",
        ),
        (
            "psycopg.connect(row_factory=rows.dict_row)",
            "psycopg.ServerCursor(conn, 'foo', row_factory=rows.namedtuple_row)",
            "psycopg.ServerCursor[NamedTuple]",
        ),
        # Async server-side cursors
        (
            "await psycopg.AsyncConnection.connect()",
            "psycopg.AsyncServerCursor(conn, name='foo')",
            "psycopg.AsyncServerCursor[Tuple[Any, ...]]",
        ),
        (
            "await psycopg.AsyncConnection.connect(row_factory=rows.dict_row)",
            "psycopg.AsyncServerCursor(conn, name='foo')",
            "psycopg.AsyncServerCursor[Dict[str, Any]]",
        ),
        (
            "await psycopg.AsyncConnection.connect()",
            "psycopg.AsyncServerCursor(conn, name='foo', row_factory=rows.dict_row)",
            "psycopg.AsyncServerCursor[Dict[str, Any]]",
        ),
    ],
)
def test_cursor_type_init(conn, curs, type, mypy):
    stmts = f"""\
conn = {conn}
obj = {curs}
"""
    _test_reveal(stmts, type, mypy)


@pytest.mark.parametrize(
    "factory, type",
    [
        ("", "Tuple[Any, ...]"),
        ("row_factory=rows.tuple_row", "Tuple[Any, ...]"),
        ("row_factory=rows.dict_row", "Dict[str, Any]"),
        ("row_factory=thing_row", "Thing"),
    ],
)
@pytest.mark.parametrize("server_side", [False, True])
@pytest.mark.parametrize("conn_class", ["Connection", "AsyncConnection"])
@pytest.mark.parametrize(
    "fetch, typemod",
    [
        ("one", "{type} | None"),
        ("many", "list[{type}]"),
        ("all", "list[{type}]"),
        ("iter", "{type}"),
        ("next", "{type}"),
    ],
)
def test_fetch_type(conn_class, server_side, factory, type, fetch, typemod, mypy):
    if "Async" in conn_class:
        async_ = "async "
        await_ = "await "
        next_ = "anext"
    else:
        async_ = await_ = ""
        next_ = "next"

    curs = f"conn.cursor({factory})"
    if server_side:
        curs = curs.replace("(", "(name='foo',", 1)

    stmts = f"""\
conn = {await_} psycopg.{conn_class}.connect()
curs = {curs}
"""
    if fetch == "one":
        stmts += f"obj = {await_} curs.fetchone()"
    elif fetch == "many":
        stmts += f"obj = {await_} curs.fetchmany(5)"
    elif fetch == "all":
        stmts += f"obj = {await_} curs.fetchall()"
    elif fetch == "iter":
        stmts += f"{async_}for obj in curs: pass"
    elif fetch == "next":
        stmts += f"obj = {await_} {next_}(curs)"
    else:
        pytest.fail(f"unexpected fetch: {fetch}")

    _test_reveal(stmts, typemod.format(type=type), mypy)


@pytest.mark.parametrize("server_side", [False, True])
@pytest.mark.parametrize("conn_class", ["Connection", "AsyncConnection"])
def test_cur_subclass_execute(mypy, conn_class, server_side):
    async_ = "async " if "Async" in conn_class else ""
    await_ = "await" if "Async" in conn_class else ""
    cur_base_class = "".join(
        [
            "Async" if "Async" in conn_class else "",
            "Server" if server_side else "",
            "Cursor",
        ]
    )
    cur_name = "'foo'" if server_side else ""

    src = f"""\
from typing import Any, cast
import psycopg
from psycopg.rows import Row, TupleRow

class MyCursor(psycopg.{cur_base_class}[Row]):
    pass

{async_}def test() -> None:
    conn = {await_} psycopg.{conn_class}.connect()

    cur: MyCursor[TupleRow]
    reveal_type(cur)

    cur = cast(MyCursor[TupleRow], conn.cursor({cur_name}))
    {async_}with cur as cur2:
        reveal_type(cur2)
        cur3 = {await_} cur2.execute("")
        reveal_type(cur3)
"""
    cp = mypy.run_on_source(src)
    out = cp.stdout.decode("utf8", "replace").splitlines()
    assert len(out) == 3
    types = [mypy.get_revealed(line) for line in out]
    assert types[0] == types[1]
    assert types[0] == types[2]


def _test_reveal(stmts, type, mypy):
    ignore = "" if type.endswith("| None") else "# type: ignore[assignment]"
    stmts = "\n".join(f"    {line}" for line in stmts.splitlines())

    src = f"""\
from __future__ import annotations

from typing import Any, Callable, Dict, NamedTuple, Sequence, Tuple
import psycopg
from psycopg import rows

class Thing:
    def __init__(self, **kwargs: Any) -> None:
        self.kwargs = kwargs

def thing_row(
    cur: psycopg.Cursor[Any] | psycopg.AsyncCursor[Any],
) -> Callable[[Sequence[Any]], Thing]:
    assert cur.description
    names = [d.name for d in cur.description]

    def make_row(t: Sequence[Any]) -> Thing:
        return Thing(**dict(zip(names, t)))

    return make_row

async def tmp() -> None:
{stmts}
    reveal_type(obj)

ref: {type} = None  {ignore}
reveal_type(ref)
"""
    cp = mypy.run_on_source(src)
    out = cp.stdout.decode("utf8", "replace").splitlines()
    assert len(out) == 2, "\n".join(out)
    got, want = (mypy.get_revealed(line) for line in out)
    assert got == want


@pytest.mark.parametrize(
    "conn, type",
    [
        (
            "MyConnection.connect()",
            "MyConnection[Tuple[Any, ...]]",
        ),
        (
            "MyConnection.connect(row_factory=rows.tuple_row)",
            "MyConnection[Tuple[Any, ...]]",
        ),
        (
            "MyConnection.connect(row_factory=rows.dict_row)",
            "MyConnection[Dict[str, Any]]",
        ),
    ],
)
def test_generic_connect(conn, type, mypy):
    src = f"""
from typing import Any, Dict, Tuple
import psycopg
from psycopg import rows

class MyConnection(psycopg.Connection[rows.Row]):
    pass

obj = {conn}
reveal_type(obj)

ref: {type} = None  # type: ignore[assignment]
reveal_type(ref)
"""
    cp = mypy.run_on_source(src)
    out = cp.stdout.decode("utf8", "replace").splitlines()
    assert len(out) == 2, "\n".join(out)
    got, want = (mypy.get_revealed(line) for line in out)
    assert got == want
