"""System, behavior, and user-acceptance style tests for DataTable."""

from pathlib import Path
from datetime import date
from types import SimpleNamespace

import pytest

import soigia.datatable as dtpkg
from soigia.datatable import DataTable


def test_system_pipeline_and_lookup_coverage(monkeypatch):
    base = DataTable(
        {
            "id": [1, 2, 3],
            "name": ["alice", "bob", "carol"],
            "score": [10, 20, 30],
            "when": ["2026-01-01", "2026-02-01", "2026-03-01"],
            "nullable": [1, None, 3],
        }
    )

    assert base.qs.count() == 3
    assert base.qs.values("name").first() == {"name": "alice"}
    assert base.qs.only().count() == 3
    assert base.qs.exclude_fields().count() == 3
    assert base.qs.filter(score__gt=10).count() == 2
    assert base.qs.filter(score__ne=20).count() == 2
    assert base.qs.filter(score__gte=20).count() == 2
    assert base.qs.filter(score__lt=30).count() == 2
    assert base.qs.filter(score__lte=20).count() == 2
    assert base.qs.filter(name__in=["alice", "carol"]).count() == 2
    assert base.qs.filter(name__contains="li").count() == 1
    assert base.qs.filter(name__icontains="ALI").count() == 1
    assert base.qs.filter(name__startswith="a").count() == 1
    assert base.qs.filter(name__istartswith="A").count() == 1
    assert base.qs.filter(name__endswith="l").count() == 1
    assert base.qs.filter(name__iendswith="L").count() == 1
    assert base.qs.filter(nullable__isnull=True).count() == 1
    assert base.qs.filter(score__range=(10, 20)).count() == 2
    assert base.qs.filter(score__between=(10, 20)).count() == 2
    assert base.qs.filter(when__year=2026).count() == 3
    assert base.qs.filter(when__month=2).count() == 1
    assert base.qs.filter(when__day=1).count() == 3
    assert base.qs.filter(when__date=date(2026, 1, 1)).count() == 1

    with pytest.raises(ValueError):
        base.qs.filter(score__unsupported=1)

    with pytest.raises(ValueError):
        base.qs.values_list("name", "score", flat=True)

    # Exercise the false branch in helper-specific render logic.
    monkeypatch.setattr(dtpkg, "openpyxl", None, raising=False)
    monkeypatch.setattr(dtpkg, "load_workbook", None, raising=False)
    monkeypatch.setattr(dtpkg, "Path", Path, raising=False)
    assert base.to_markdown_table().startswith("|")


def test_behavior_diff_and_schema_variants(monkeypatch):
    left = DataTable(
        {
            "id": [1, 2],
            "name": ["alice", "bob"],
            "age": [19, 24],
            "note": ["A", "B"],
        }
    ).auto_version("seed")
    right = DataTable(
        {
            "id": [1, 2, 3],
            "name": ["alice", "bobby", "carol"],
            "age": [19, 25, 31],
            "note": ["A", "C", "D"],
        }
    )

    assert left.diff_columns_against(right)["added_columns"] == []
    assert left.diff_added_rows(right, key_columns=["id"]).shape[0] == 1
    assert left.diff_removed_rows(right, key_columns=["id"]).shape[0] == 0
    assert left.diff_unchanged_rows(right, key_columns=["id"]).shape[0] == 1
    assert left.diff_changed_rows(right, key_columns=["id"]).shape[0] == 1
    assert left.diff_changed_cells(right, key_columns=["id"]).shape[0] == 3
    assert left.diff_changed_cells(right).shape[0] >= 0
    assert left.diff_report(right, key_columns=["id"])["has_changes"] is True
    assert left.different_rows(right, key_columns=["id"])["changed_count"] == 1
    assert left.compare_frames(right, key_columns=["id"])["changed_count"] == 1
    assert left.diff_versions()["row_delta"] == 0
    assert left.diff_records()["row_delta"] == 0
    assert left.diff_summary(key_columns=["id"])["changed_rows"] >= 0

    assert left.validate_schema({"name": {"nullable": False}}).equals(left)
    assert left.validate_schema({"id": int}).equals(left)
    assert left.validate_schema({"id": "ignored"}).equals(left)
    assert left.validate_schema({"missing": {"required": False}}).equals(left)
    assert left.validate_schema({"id": {"type": (int, float)}}).equals(left)
    assert left.validate_schema({"name": {"choices": ["alice", "bob"]}}).equals(left)
    assert left.validate_schema({"name": {"regex": "^[ab]"}}).equals(left)
    assert DataTable({"name": ["abcde"]}).validate_schema({"name": {"length": 5}}).equals(DataTable({"name": ["abcde"]}))
    assert left.validate_schema({"name": {"length": {"min": 3, "max": 5}}}).equals(left)
    assert left.validate_schema({"name": {"length": {"min": 2}}}).equals(left)
    assert left.validate_schema({"name": {"length": {"max": 10}}}).equals(left)
    assert left.validate_schema({"name": {"length": "ignored"}}).equals(left)

    with pytest.raises(ValueError):
        left.validate_schema({"name": {"nullable": False}}, strict=True)
    with pytest.raises(KeyError):
        left.validate_schema({"missing": {"required": True}})
    with pytest.raises(TypeError):
        left.validate_schema({"id": {"type": str}})
    with pytest.raises(ValueError):
        left.validate_schema({"name": {"choices": ["zzz"]}})
    with pytest.raises(ValueError):
        left.validate_schema({"name": {"regex": "^z"}})
    with pytest.raises(ValueError):
        left.validate_schema({"name": {"length": 2}})
    with pytest.raises(ValueError):
        left.validate_schema({"name": {"length": {"min": 6}}})
    with pytest.raises(ValueError):
        left.validate_schema({"name": {"length": {"max": 3}}})
    with pytest.raises(ValueError):
        left.validate_schema({"id": {"min": 20}})
    with pytest.raises(ValueError):
        left.validate_schema({"id": {"max": 1}})

    assert left.ensure_columns("extra").columns.tolist()[-1] == "extra"
    assert left.ensure_columns().equals(left)
    assert left.drop_empty_columns().equals(left)
    assert left.fill_missing(0).equals(left)
    assert left.fill_missing(0, columns=["name"]).equals(left)
    assert left.only("id").columns.tolist() == ["id"]
    assert left.only().equals(left)
    assert left.select("id").columns.tolist() == ["id"]
    assert left.select().equals(left)
    assert left.exclude_fields("note").columns.tolist() == ["id", "name", "age"]
    assert left.exclude_fields().equals(left)
    assert left.diff_changed_cells(right, key_columns=["id"]).shape[0] == 3
    assert left.diff_changed_cells(right).shape[0] >= 0
    with pytest.raises(KeyError):
        left.diff_changed_cells(right, key_columns=["missing"])
    with pytest.raises(KeyError):
        left.diff_report(right, key_columns=["missing"])
    assert left.objects.get_or_create(name="new", defaults={"age": 18})[1] is True
    assert left.objects.update_or_create(name="alice", defaults={"age": 20})[1] is False
    assert left.objects.group_by("id").aggregate(total="age")["total"]


def test_uat_writer_and_export_paths(monkeypatch):
    table = DataTable({"id": [1], "name": ["alice"], "age": [19]})

    assert table.objects.bulk_create([]).equals(table)
    updated = table.objects.bulk_update({"age": 20}, age__gte=19)
    assert updated["age"].iloc[0] == 20
    updated2, count = table.objects.bulk_update({"age": 20}, return_count=True, age__gte=19)
    assert count == 1
    assert updated2["age"].iloc[0] == 20
    assert table.objects.bulk_update({"note": "x"})["note"].iloc[0] == "x"
    assert table.objects.get_or_create(name="alice", defaults={"age": 19})[1] is False
    assert table.objects.update_or_create(name="alice", defaults={"age": 21})[1] is False
    assert table.objects.update_or_create(name="new", defaults={"age": 18})[1] is True

    assert table.objects.diff_report(table, key_columns=["id"])["has_changes"] is False
    assert table.objects.diff_columns_against(table)["column_order_changed"] is False
    assert table.objects.diff_changed_cells(table).shape[0] == 0

    fake_openpyxl = type("FakeOpenpyxlModule", (), {})()
    monkeypatch.setitem(dtpkg.__dict__, "openpyxl", fake_openpyxl)
    monkeypatch.setattr(DataTable, "to_excel", lambda self, output, **kwargs: Path(output).write_text("x", encoding="utf-8"), raising=False)
    path = Path("tests") / "_uat.xlsx"
    try:
        assert table.to_excel_table(path).exists()
    finally:
        if path.exists():
            path.unlink()


def test_render_and_import_fallback_branches(monkeypatch):
    table = DataTable({"a": [1], "b": [None]})

    monkeypatch.delitem(dtpkg.__dict__, "openpyxl", raising=False)
    monkeypatch.delitem(dtpkg.__dict__, "load_workbook", raising=False)
    assert table.drop_empty_columns().columns.tolist() == ["a"]
    assert table.render_table("rich") is not None
    with pytest.raises(ValueError):
        table.render_table("excel")
    with pytest.raises(ValueError):
        table.render_table("unknown")

    # direct helper coverage for unsupported lookup and null-equality branch
    assert dtpkg.DataTable._cell_equal(None, None) is True
    assert dtpkg.DataTable._cell_equal(float("nan"), float("nan")) is True
    with pytest.raises(ValueError):
        dtpkg._ORMQuerySet._apply_lookup(table["a"], "unsupported", 1)
    assert bool(dtpkg._ORMQuerySet._apply_lookup(table["a"], "exact", 1).iloc[0]) is True
    assert dtpkg.DataTable._cell_equal(dtpkg.pd.NA, dtpkg.pd.NA) is True


def test_queryset_render_delegation_branches(monkeypatch):
    table = DataTable({"name": ["alice"], "age": [19]})
    qs = table.qs
    path = Path("tests") / "_qs.xlsx"

    monkeypatch.setattr(DataTable, "to_rich_table", lambda self, *args, **kwargs: "RICH", raising=False)
    monkeypatch.setattr(DataTable, "to_excel_table", lambda self, *args, **kwargs: path, raising=False)

    assert qs.to_rich_table() == "RICH"
    assert qs.to_excel_table(path) == path


def test_google_client_builder_branches(monkeypatch):
    fake_gspread = SimpleNamespace(
        authorize=lambda credentials: {"client": credentials},
        service_account=lambda filename=None: {"service_account": filename or "default"},
    )

    class FakeCredentials:
        @classmethod
        def from_service_account_info(cls, info, scopes=None):
            return {"info": info, "scopes": scopes}

        @classmethod
        def from_service_account_file(cls, filename, scopes=None):
            return {"file": filename, "scopes": scopes}

    fake_google = SimpleNamespace(oauth2=SimpleNamespace(service_account=SimpleNamespace(Credentials=FakeCredentials)))
    monkeypatch.setitem(__import__("sys").modules, "gspread", fake_gspread)
    monkeypatch.setitem(__import__("sys").modules, "google", fake_google)
    monkeypatch.setitem(__import__("sys").modules, "google.oauth2", fake_google.oauth2)
    monkeypatch.setitem(
        __import__("sys").modules, "google.oauth2.service_account", fake_google.oauth2.service_account
    )

    client_from_json = dtpkg._build_google_client(credentials_json='{"x": 1}', scopes=["scope-1"])
    assert client_from_json["client"]["info"] == {"x": 1}
    client_from_file = dtpkg._build_google_client(service_account_file="creds.json", scopes=["scope-2"])
    assert client_from_file["client"]["file"] == "creds.json"
    assert dtpkg._build_google_client(client={"prebuilt": True}) == {"prebuilt": True}
    assert dtpkg._build_google_client() == {"service_account": "default"}
