"""Tests for bitable view config conditional formatting validation (U5 / R4).

Covers all 7 operators + 8 color keys. Layered like test_grouping.py:
1. Pure Pydantic validation (no DB) — always runs
2. Route-level 422 via fake service (no DB) — always runs
3. Round-trip via real PG service — skipped if PG unavailable
"""

from __future__ import annotations

from typing import Any

import httpx
import pytest
from fastapi import FastAPI
from httpx import ASGITransport
from pydantic import ValidationError

from agentkit.bitable.models import View, ViewType
from agentkit.bitable.view_config import (
    ColorKey,
    ConditionalFormatRule,
    ConditionalOperator,
    ViewConfigSchema,
    ViewConfigValidationError,
    validate_view_config,
)
from agentkit.server.routes import bitable as bitable_routes
from agentkit.server.routes.bitable import require_bitable_auth

TEST_USER_ID = "test-user-id"

# Exhaustive lists — kept in sync with the Literal definitions in view_config.py.
ALL_OPERATORS: list[ConditionalOperator] = [
    "equals",
    "not-equals",
    "contains",
    "is-empty",
    "greater-than",
    "less-than",
    "between",
]
ALL_COLOR_KEYS: list[ColorKey] = [
    "red",
    "orange",
    "yellow",
    "green",
    "blue",
    "purple",
    "gray",
    "neutral",
]


def _rule(
    *,
    field_id: str = "f1",
    operator: ConditionalOperator = "equals",
    value: str = "v",
    color_key: ColorKey = "red",
    bold: bool = True,
    enabled: bool = True,
) -> dict[str, object]:
    return {
        "field_id": field_id,
        "operator": operator,
        "value": value,
        "color_key": color_key,
        "bold": bold,
        "enabled": enabled,
    }


# ---------------------------------------------------------------------------
# 1. Pure Pydantic validation — always runs
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("op", ALL_OPERATORS)
def test_validate_accepts_each_operator(op: ConditionalOperator) -> None:
    """All 7 operators accepted (spec test scenario 8)."""
    config = {"conditional_formatting": [_rule(operator=op)]}
    validate_view_config(config)  # no raise


@pytest.mark.parametrize("color", ALL_COLOR_KEYS)
def test_validate_accepts_each_color_key(color: ColorKey) -> None:
    """All 8 color keys accepted."""
    config = {"conditional_formatting": [_rule(color_key=color)]}
    validate_view_config(config)  # no raise


def test_validate_rejects_invalid_operator() -> None:
    """Operator not in the 7-key whitelist is rejected."""
    config = {"conditional_formatting": [_rule(operator="starts-with")]}  # type: ignore[dict-item]
    with pytest.raises(ViewConfigValidationError):
        validate_view_config(config)


def test_validate_rejects_invalid_color_key() -> None:
    """color_key not in the 8-color whitelist is rejected."""
    config = {"conditional_formatting": [_rule(color_key="pink")]}  # type: ignore[dict-item]
    with pytest.raises(ViewConfigValidationError):
        validate_view_config(config)


def test_validate_rejects_missing_field_id() -> None:
    """field_id is required."""
    rule = _rule()
    del rule["field_id"]
    config = {"conditional_formatting": [rule]}
    with pytest.raises(ViewConfigValidationError):
        validate_view_config(config)


def test_validate_rejects_missing_operator() -> None:
    """operator is required."""
    rule = _rule()
    del rule["operator"]
    config = {"conditional_formatting": [rule]}
    with pytest.raises(ViewConfigValidationError):
        validate_view_config(config)


def test_validate_rejects_missing_color_key() -> None:
    """color_key is required."""
    rule = _rule()
    del rule["color_key"]
    config = {"conditional_formatting": [rule]}
    with pytest.raises(ViewConfigValidationError):
        validate_view_config(config)


def test_validate_value_defaults_to_empty_string() -> None:
    """value is optional, defaults to '' (UI sends '' for is-empty)."""
    rule = _rule()
    del rule["value"]
    parsed = ConditionalFormatRule.model_validate(rule)
    assert parsed.value == ""


def test_validate_bold_defaults_to_true() -> None:
    """bold defaults to True (WCAG 1.4.1 — color alone is not enough)."""
    rule = _rule()
    del rule["bold"]
    parsed = ConditionalFormatRule.model_validate(rule)
    assert parsed.bold is True


def test_validate_enabled_defaults_to_true() -> None:
    """enabled defaults to True."""
    rule = _rule()
    del rule["enabled"]
    parsed = ConditionalFormatRule.model_validate(rule)
    assert parsed.enabled is True


def test_validate_rejects_extra_keys_in_rule() -> None:
    """extra='forbid' on ConditionalFormatRule — unknown keys rejected."""
    rule = _rule()
    rule["extra_key"] = "oops"
    config = {"conditional_formatting": [rule]}
    with pytest.raises(ViewConfigValidationError):
        validate_view_config(config)


def test_validate_accepts_empty_conditional_formatting_list() -> None:
    """Empty rule list is valid (no rules = no coloring)."""
    validate_view_config({"conditional_formatting": []})


def test_validate_accepts_multiple_rules() -> None:
    """Multiple rules accepted (first-match-wins is enforced client-side)."""
    config = {
        "conditional_formatting": [
            _rule(field_id="f1", operator="equals", value="a", color_key="red"),
            _rule(field_id="f2", operator="greater-than", value="10", color_key="green"),
            _rule(field_id="f3", operator="is-empty", color_key="gray"),
        ]
    }
    validate_view_config(config)  # no raise


def test_validate_ignores_non_u5_keys_with_cf_present() -> None:
    """filters / sort / hidden_fields pass through when cf is also present."""
    config = {
        "filters": [{"field_id": "f1", "op": "weird-op"}],
        "hidden_fields": ["f2"],
        "conditional_formatting": [_rule()],
    }
    validate_view_config(config)  # no raise


def test_schema_round_trips_conditional_formatting() -> None:
    """ViewConfigSchema round-trips a CF config without loss."""
    raw = {
        "group_by": [],
        "conditional_formatting": [
            {
                "field_id": "f1",
                "operator": "between",
                "value": "10,20",
                "color_key": "blue",
                "bold": False,
                "enabled": True,
            }
        ],
    }
    schema = ViewConfigSchema.model_validate(raw)
    assert len(schema.conditional_formatting) == 1
    rule = schema.conditional_formatting[0]
    assert rule.operator == "between"
    assert rule.color_key == "blue"
    assert rule.bold is False
    dumped = schema.model_dump()
    assert dumped["conditional_formatting"][0]["value"] == "10,20"


def test_rule_model_rejects_invalid_operator_directly() -> None:
    """ConditionalFormatRule rejects invalid operator at construction."""
    with pytest.raises(ValidationError):
        ConditionalFormatRule(
            field_id="f1",
            operator="starts-with",  # type: ignore[arg-type]
            value="v",
            color_key="red",
        )


def test_rule_model_rejects_invalid_color_key_directly() -> None:
    """ConditionalFormatRule rejects invalid color_key at construction."""
    with pytest.raises(ValidationError):
        ConditionalFormatRule(
            field_id="f1",
            operator="equals",
            value="v",
            color_key="pink",  # type: ignore[arg-type]
        )


def test_constants_match_spec() -> None:
    """7 operators + 8 color keys (spec invariant)."""
    assert len(ALL_OPERATORS) == 7
    assert len(ALL_COLOR_KEYS) == 8


# ---------------------------------------------------------------------------
# 2. Route-level 422 — uses a fake service (no DB)
# ---------------------------------------------------------------------------


def _make_fake_view(view_id: str = "v1", table_id: str = "t1") -> View:
    return View(
        id=view_id,
        table_id=table_id,
        name="Test View",
        view_type=ViewType.grid,
        config={},
    )


class _FakeService:
    """Minimal service stub — same shape as test_grouping.py's stub."""

    def __init__(self, *, existing_view: View | None = None) -> None:
        self._existing = existing_view

    async def get_view(self, view_id: str) -> View | None:
        if self._existing is None or self._existing.id != view_id:
            return None
        return self._existing

    async def get_table(self, table_id: str) -> Any:
        class _T:
            owner_user_id = TEST_USER_ID

        return _T()

    async def update_view(self, view_id: str, **kwargs: object) -> View | None:
        if self._existing is None:
            return None
        config = kwargs.get("config")
        if isinstance(config, dict):
            self._existing = self._existing.model_copy(update={"config": config})
        return self._existing


@pytest.fixture
def fake_service_app() -> FastAPI:
    app = FastAPI()
    app.state.bitable_service = _FakeService(existing_view=_make_fake_view())
    app.include_router(bitable_routes.router, prefix="/api/v1")
    app.dependency_overrides[require_bitable_auth] = lambda: {
        "user_id": TEST_USER_ID,
        "username": "testuser",
        "role": "member",
    }
    return app


@pytest.fixture
async def fake_client(fake_service_app: FastAPI) -> httpx.AsyncClient:
    transport = ASGITransport(app=fake_service_app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


async def test_patch_view_with_valid_cf_returns_200(
    fake_client: httpx.AsyncClient,
) -> None:
    """Valid conditional_formatting is accepted and persisted."""
    config = {
        "conditional_formatting": [
            {
                "field_id": "f1",
                "operator": "equals",
                "value": "done",
                "color_key": "green",
            }
        ]
    }
    resp = await fake_client.patch(
        "/api/v1/bitable/views/v1",
        json={"config": config},
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["view"]["config"]["conditional_formatting"] == config["conditional_formatting"]


async def test_patch_view_with_invalid_operator_returns_422(
    fake_client: httpx.AsyncClient,
) -> None:
    """Invalid operator returns 422."""
    resp = await fake_client.patch(
        "/api/v1/bitable/views/v1",
        json={
            "config": {
                "conditional_formatting": [
                    {"field_id": "f1", "operator": "starts-with", "value": "x", "color_key": "red"}
                ]
            }
        },
    )
    assert resp.status_code == 422


async def test_patch_view_with_invalid_color_key_returns_422(
    fake_client: httpx.AsyncClient,
) -> None:
    """Invalid color_key returns 422."""
    resp = await fake_client.patch(
        "/api/v1/bitable/views/v1",
        json={
            "config": {
                "conditional_formatting": [
                    {"field_id": "f1", "operator": "equals", "value": "x", "color_key": "pink"}
                ]
            }
        },
    )
    assert resp.status_code == 422


async def test_patch_view_with_missing_field_id_returns_422(
    fake_client: httpx.AsyncClient,
) -> None:
    """Missing field_id in a CF rule returns 422."""
    resp = await fake_client.patch(
        "/api/v1/bitable/views/v1",
        json={
            "config": {
                "conditional_formatting": [{"operator": "equals", "value": "x", "color_key": "red"}]
            }
        },
    )
    assert resp.status_code == 422


# ---------------------------------------------------------------------------
# 3. Round-trip via real PG — skipped if PG unavailable
# ---------------------------------------------------------------------------


@pytest.mark.postgres
async def test_round_trip_conditional_formatting_via_real_service(bitable_service) -> None:
    """PATCH config with conditional_formatting → GET view returns same."""
    from agentkit.bitable.service import BitableService

    assert isinstance(bitable_service, BitableService)
    table = await bitable_service.create_table(name="U5 CF Round-Trip")
    view = await bitable_service.create_view(table.id, name="V1")

    cf_rules = [
        {
            "field_id": "f1",
            "operator": "equals",
            "value": "done",
            "color_key": "green",
            "bold": True,
            "enabled": True,
        },
        {
            "field_id": "f2",
            "operator": "between",
            "value": "10,20",
            "color_key": "blue",
            "bold": False,
            "enabled": False,
        },
    ]
    config = {"conditional_formatting": cf_rules}
    validate_view_config(config)
    updated = await bitable_service.update_view(view.id, config=config)
    assert updated is not None
    assert updated.config.get("conditional_formatting") == cf_rules

    fetched = await bitable_service.get_view(view.id)
    assert fetched is not None
    assert fetched.config.get("conditional_formatting") == cf_rules
