import sys
import types

import pandas as pd
import pytest

from mnemosynecore.data_bases import vertica


class DummyCursor:
    def __init__(self, fail=False):
        self.fail = fail
        self.calls = []
        self.description = [("id",), ("name",)]
        self._rows = [(1, "alpha"), (2, "beta")]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        return False

    def execute(self, sql, params=None):
        if self.fail:
            raise RuntimeError("sql-fail")
        self.calls.append(("execute", sql, params))

    def executemany(self, sql, params=None):
        if self.fail:
            raise RuntimeError("sql-fail")
        self.calls.append(("executemany", sql, params))

    def fetchall(self):
        return self._rows

    def fetchone(self):
        return self._rows[0] if self._rows else None


class DummyConn:
    def __init__(self, fail=False):
        self.cursor_obj = DummyCursor(fail=fail)
        self.commit_calls = 0
        self.rollback_calls = 0
        self.close_calls = 0

    def cursor(self):
        return self.cursor_obj

    def commit(self):
        self.commit_calls += 1

    def rollback(self):
        self.rollback_calls += 1

    def close(self):
        self.close_calls += 1


def test_read_sql_file_returns_content(tmp_path):
    sql_file = tmp_path / "q.sql"
    sql_file.write_text("select 1;", encoding="utf-8")
    assert vertica.read_sql_file(str(sql_file)) == "select 1;"


def test_read_sql_file_missing_returns_none(tmp_path):
    assert vertica.read_sql_file(str(tmp_path / "none.sql")) is None


def test_load_sql_tasks_from_dir_raises_without_airflow(monkeypatch):
    monkeypatch.setattr(vertica, "HAS_AIRFLOW", False)
    with pytest.raises(ImportError):
        vertica.load_sql_tasks_from_dir("/tmp", "CID")


def test_load_sql_tasks_from_dir_success(monkeypatch, tmp_path):
    class FakeVerticaOperator:
        def __init__(self, **kwargs):
            self.kwargs = kwargs

    module = types.ModuleType("airflow.providers.vertica.operators.vertica")
    module.VerticaOperator = FakeVerticaOperator
    monkeypatch.setitem(sys.modules, "airflow.providers.vertica.operators.vertica", module)
    monkeypatch.setattr(vertica, "HAS_AIRFLOW", True)
    monkeypatch.setattr(vertica, "get_current_context", lambda: {"dag": "DAG1"}, raising=False)

    (tmp_path / "a.sql").write_text("select 1;select 2;", encoding="utf-8")
    (tmp_path / "b.txt").write_text("skip", encoding="utf-8")

    tasks = vertica.load_sql_tasks_from_dir(str(tmp_path), "VERT")
    assert "task_a_vertica" in tasks
    assert tasks["task_a_vertica"].kwargs["vertica_conn_id"] == "VERT"


def test_vertica_dedupe_requires_conn_or_id():
    with pytest.raises(ValueError):
        vertica.vertica_dedupe("t", ["k1"])


def test_vertica_dedupe_builds_sql_for_all_keys(monkeypatch):
    captured = {}

    def fake_vertica_sql(**kwargs):
        captured.update(kwargs)

    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_sql", fake_vertica_sql)
    vertica.vertica_dedupe(table_name="tbl", unique_keys=["k1", "k2"], conn=conn, date_col="dt")
    sql = captured["sql"]
    assert "t.k1 = x.k1 AND t.k2 = x.k2" in sql
    assert "ORDER BY dt DESC" in sql


def test_vertica_get_connection_as_json_delegates(monkeypatch):
    monkeypatch.setattr(vertica, "_get_connection_as_json", lambda name: "ok-json")
    assert vertica.get_connection_as_json("CID") == "ok-json"


def test_vertica_upsert_empty_df_returns_early():
    conn = DummyConn()
    vertica.vertica_upsert(pd.DataFrame(), "tbl", "id", conn=conn)
    assert conn.commit_calls == 0


def test_vertica_upsert_with_conn(monkeypatch):
    calls = []

    def fake_vertica_sql(**kwargs):
        calls.append(kwargs)

    conn = DummyConn()
    df = pd.DataFrame([{"id": 1, "dt": "2026-01-01", "val": 10}])
    monkeypatch.setattr(vertica, "vertica_sql", fake_vertica_sql)
    vertica.vertica_upsert(df=df, table_name="tbl", unique_keys="id", conn=conn, date_col="dt", days_back=7, commit=True)

    assert len(calls) == 4
    assert conn.commit_calls == 1
    assert conn.close_calls == 0


def test_vertica_upsert_with_conn_id_closes_conn(monkeypatch):
    calls = []
    conn = DummyConn()
    df = pd.DataFrame([{"id": 1, "val": 10}])
    monkeypatch.setattr(vertica, "vertica_sql", lambda **kwargs: calls.append(kwargs))
    monkeypatch.setattr(vertica, "vertica_conn", lambda conn_id: conn)
    vertica.vertica_upsert(df=df, table_name="tbl", unique_keys="id", conn_id="CID", commit=False)

    assert len(calls) == 3
    assert conn.commit_calls == 0
    assert conn.close_calls == 1


def test_vertica_conn_uses_secret_and_extra(monkeypatch):
    captured = {}
    monkeypatch.setattr(
        vertica,
        "get_secret",
        lambda conn_id: {
            "host": "vh",
            "port": 5433,
            "login": "u",
            "password": "p",
            "schema": "db",
            "extra": '{"session_label":"x"}',
        },
    )
    monkeypatch.setattr(vertica.vertica_python, "connect", lambda **kwargs: captured.setdefault("kwargs", kwargs) or "CONN")
    out = vertica.vertica_conn("CID")
    assert out == captured["kwargs"]
    assert captured["kwargs"]["database"] == "db"
    assert captured["kwargs"]["session_label"] == "x"


def test_vertica_conn_supports_user_field(monkeypatch):
    captured = {}
    monkeypatch.setattr(
        vertica,
        "get_secret",
        lambda conn_id: {
            "host": "vh",
            "port": 5433,
            "user": "from_user",
            "password": "p",
            "schema": "db",
        },
    )
    monkeypatch.setattr(
        vertica.vertica_python,
        "connect",
        lambda **kwargs: captured.setdefault("kwargs", kwargs) or "CONN",
    )
    vertica.vertica_conn("CID")
    assert captured["kwargs"]["user"] == "from_user"


def test_get_vertica_engine(monkeypatch):
    monkeypatch.setattr(vertica, "un_conn", lambda conn_id, conn_type: ("E", conn_id, conn_type))
    assert vertica.get_vertica_engine("CID") == ("E", "CID", "vertica")


def test_vertica_sql_requires_conn_or_id():
    with pytest.raises(ValueError):
        vertica.vertica_sql(sql="select 1")


def test_vertica_sql_execute_and_commit():
    conn = DummyConn()
    vertica.vertica_sql(conn=conn, sql="select 1", params={"a": 1}, commit=True)
    assert conn.cursor_obj.calls[0][0] == "execute"
    assert conn.commit_calls == 1


def test_vertica_sql_executemany():
    conn = DummyConn()
    rows = [(1,), (2,)]
    vertica.vertica_sql(conn=conn, sql="insert", params=rows, many=True)
    assert conn.cursor_obj.calls[0][0] == "executemany"


def test_vertica_sql_rollbacks_and_reraises():
    conn = DummyConn(fail=True)
    with pytest.raises(RuntimeError, match="sql-fail"):
        vertica.vertica_sql(conn=conn, sql="broken")
    assert conn.rollback_calls == 1


def test_vertica_sql_conn_id_closes(monkeypatch):
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_conn", lambda conn_id: conn)
    vertica.vertica_sql(conn_id="CID", sql="select 1")
    assert conn.close_calls == 1


def test_vertica_select_requires_conn_or_id():
    with pytest.raises(ValueError):
        vertica.vertica_select(sql="select 1")


def test_vertica_select_returns_dataframe():
    conn = DummyConn()
    df = vertica.vertica_select(conn=conn, sql="select * from t")
    assert list(df.columns) == ["id", "name"]
    assert df.shape == (2, 2)


def test_vertica_select_conn_id_closes(monkeypatch):
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_conn", lambda conn_id: conn)
    vertica.vertica_select(conn_id="CID", sql="select 1")
    assert conn.close_calls == 1


def test_split_sql_statements():
    assert vertica.split_sql_statements("select 1; select 2;") == ["select 1", "select 2"]
    assert vertica.split_sql_statements("") == []


def test_vertica_sql_file_not_found(tmp_path):
    with pytest.raises(FileNotFoundError):
        vertica.vertica_sql_file(file_path=str(tmp_path / "x.sql"), conn=DummyConn())


def test_vertica_sql_file_executes_statements(monkeypatch, tmp_path):
    sql_file = tmp_path / "q.sql"
    sql_file.write_text("select 1;select 2;", encoding="utf-8")
    calls = []
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_sql", lambda **kwargs: calls.append(kwargs))
    executed = vertica.vertica_sql_file(file_path=str(sql_file), conn=conn)
    assert executed == 2
    assert len(calls) == 2
    assert conn.commit_calls == 1


def test_vertica_sql_file_uses_conn_id(monkeypatch, tmp_path):
    sql_file = tmp_path / "q.sql"
    sql_file.write_text("select 1;", encoding="utf-8")
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_conn", lambda conn_id: conn)
    monkeypatch.setattr(vertica, "vertica_sql", lambda **kwargs: None)
    executed = vertica.vertica_sql_file(file_path=str(sql_file), conn_id="CID")
    assert executed == 1
    assert conn.close_calls == 1


def test_vertica_sql_dir(monkeypatch, tmp_path):
    (tmp_path / "a.sql").write_text("select 1;", encoding="utf-8")
    (tmp_path / "b.sql").write_text("select 2;select 3;", encoding="utf-8")
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_sql_file", lambda **kwargs: 1 if kwargs["file_path"].endswith("a.sql") else 2)
    out = vertica.vertica_sql_dir(dir_sql=str(tmp_path), conn=conn)
    assert out[str(tmp_path / "a.sql")] == 1
    assert out[str(tmp_path / "b.sql")] == 2
    assert conn.commit_calls == 1


def test_vertica_sql_dir_invalid():
    with pytest.raises(NotADirectoryError):
        vertica.vertica_sql_dir(dir_sql="/no/such/dir", conn=DummyConn())


def test_vertica_select_scalar():
    conn = DummyConn()
    out = vertica.vertica_select_scalar(conn=conn, sql="select 1")
    assert out == 1


def test_vertica_select_scalar_default_when_empty():
    conn = DummyConn()
    conn.cursor_obj._rows = []
    out = vertica.vertica_select_scalar(conn=conn, sql="select 1", default="x")
    assert out == "x"


def test_vertica_insert_dataframe(monkeypatch):
    calls = []
    df = pd.DataFrame([{"id": 1}, {"id": 2}, {"id": 3}])
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_sql", lambda **kwargs: calls.append(kwargs))
    inserted = vertica.vertica_insert_dataframe(
        df=df,
        table_name="tbl",
        conn=conn,
        chunk_size=2,
    )
    assert inserted == 3
    assert len(calls) == 2
    assert conn.commit_calls == 1


def test_vertica_insert_dataframe_empty():
    inserted = vertica.vertica_insert_dataframe(
        df=pd.DataFrame(),
        table_name="tbl",
        conn=DummyConn(),
    )
    assert inserted == 0


def test_vertica_insert_dataframe_validates_chunk_size():
    with pytest.raises(ValueError):
        vertica.vertica_insert_dataframe(
            df=pd.DataFrame([{"id": 1}]),
            table_name="tbl",
            conn=DummyConn(),
            chunk_size=0,
        )


def test_vertica_table_exists_true():
    conn = DummyConn()
    assert vertica.vertica_table_exists(table_name="tbl", conn=conn) is True


def test_vertica_table_exists_false():
    conn = DummyConn()
    conn.cursor_obj._rows = []
    assert vertica.vertica_table_exists(table_name="tbl", conn=conn) is False


def test_vertica_sql_test_uses_test_conn_and_closes(monkeypatch):
    conn = DummyConn()
    monkeypatch.setattr(vertica, "vertica_conn_test", lambda conn_id, dir_path=None, creds_path=None: conn)
    vertica.vertica_sql_test(conn_id="CID", sql="select 1")
    assert conn.close_calls == 1


def test_get_vertica_engine_test(monkeypatch):
    monkeypatch.setattr(
        vertica,
        "un_conn_test",
        lambda conn_id, conn_type, dir_path=None, creds_path=None: ("E", conn_id, conn_type),
    )
    assert vertica.get_vertica_engine_test("CID") == ("E", "CID", "vertica")
