"""Data Queries: REST sources with parameters and secrets, caching, use as a
dataset/page source (filters on a parameter feed the API), role limits, and
query-backed form choice options."""

import json

import pytest

from app.core.config import settings
from app.crud import data_queries
from tests.test_enterprise_access import _form, _person, _submit

PAYLOAD = {
    "data": {
        "items": [
            {"code": "IN", "name": "India", "region": {"name": "Asia"}, "pop": 1400, "echo": "tok-123456789"},
            {"code": "FR", "name": "France", "region": {"name": "Europe"}, "pop": 68},
            {"code": "JP", "name": "Japan", "region": {"name": "Asia"}, "pop": 125},
        ]
    }
}


@pytest.fixture
def api(monkeypatch):
    """Stands in for the remote API; records every call."""
    calls: list[dict] = []

    def fake_call(method, url, headers, body):
        calls.append({"method": method, "url": url, "headers": headers, "body": body})
        return 200, json.dumps(PAYLOAD).encode()

    monkeypatch.setattr(data_queries, "_http_call", fake_call)
    monkeypatch.setattr(data_queries, "_check_host", lambda url: None)
    data_queries.clear_cache()
    yield calls
    data_queries.clear_cache()


async def _query(client, headers, **extra):
    body = {
        "name": extra.pop("name", "Countries"),
        "kind": "rest",
        "definition": {
            "url": "https://api.example.com/countries?q={{param.q}}",
            "headers": {"Authorization": "Bearer {{secret.COUNTRY_KEY}}"},
            "rows_path": "data.items",
        },
        "parameters": [{"name": "q", "default": "all"}],
        **extra,
    }
    resp = await client.post("/data-queries", headers=headers, json=body)
    assert resp.status_code == 201, resp.text
    return resp.json()


async def test_run_fills_params_and_secrets_masks_and_caches(client, auth_headers, api):
    assert (await client.post("/secrets", headers=auth_headers, json={"name": "COUNTRY_KEY", "value": "tok-123456789"})).status_code == 201
    q = await _query(client, auth_headers)

    run = await client.post(f"/data-queries/{q['id']}/run", headers=auth_headers, json={"params": {"q": "a b&c"}})
    assert run.status_code == 200, run.text
    body = run.json()
    assert api[0]["url"] == "https://api.example.com/countries?q=a%20b%26c"
    assert api[0]["headers"]["Authorization"] == "Bearer tok-123456789"
    assert body["total"] == 3 and body["cached"] is False
    assert body["rows"][0]["region.name"] == "Asia"  # nested objects flattened
    assert "tok-123456789" not in json.dumps(body)  # an echoed secret is masked

    again = await client.post(f"/data-queries/{q['id']}/run", headers=auth_headers, json={"params": {"q": "a b&c"}})
    assert again.json()["cached"] is True and len(api) == 1
    other = await client.post(f"/data-queries/{q['id']}/run", headers=auth_headers, json={"params": {}})
    assert other.json()["cached"] is False and api[-1]["url"].endswith("q=all")  # the default

    # Saving the query starts a fresh cache.
    await client.patch(f"/data-queries/{q['id']}", headers=auth_headers, json={"cache_seconds": 30})
    await client.post(f"/data-queries/{q['id']}/run", headers=auth_headers, json={"params": {}})
    assert len(api) == 3

    # Secrets' "used by" lists the query; the designer pickers see no URL.
    used = {s["name"]: s["used_by"] for s in (await client.get("/secrets", headers=auth_headers)).json()}
    assert used["COUNTRY_KEY"][0]["kind"] == "data_query"
    options = (await client.get("/data-queries/options", headers=auth_headers)).json()
    assert options[0]["name"] == "Countries" and "definition" not in options[0]


async def test_query_as_a_dataset_source(client, auth_headers, api):
    q = await _query(client, auth_headers)
    base = {"source_type": "query", "source_id": q["id"]}

    rows = await client.post(
        "/datasets/query",
        headers=auth_headers,
        json={**base, "filters": [{"field": "q", "operator": "equals", "value": "asia"}, {"field": "pop", "operator": "gt", "value": 100}], "sort_field": "pop", "sort_dir": "desc", "columns": ["name", "pop"]},
    )
    assert rows.status_code == 200, rows.text
    assert api[0]["url"].endswith("q=asia")  # the parameter filter went to the API
    assert rows.json()["rows"] == [{"name": "India", "pop": 1400}, {"name": "Japan", "pop": 125}]

    grouped = await client.post("/datasets/query", headers=auth_headers, json={**base, "group_by": "region.name", "aggregate_fn": "sum", "aggregate_field": "pop"})
    assert {r["region.name"]: r for r in grouped.json()["rows"]}["Asia"]["sum_pop"] == 1525

    series = await client.post("/datasets/query-series", headers=auth_headers, json={**base, "x_field": "region.name", "series": [{"aggregate": "count", "label": "n"}]})
    assert series.status_code == 200 and {r["x"]: r["n"] for r in series.json()["rows"]} == {"Asia": 2, "Europe": 1}

    saved = await client.post("/datasets", headers=auth_headers, json={"name": "Big countries", **base, "filters": [{"field": "pop", "operator": "gt", "value": 100}]})
    assert saved.status_code == 201, saved.text
    data = await client.get(f"/datasets/{saved.json()['id']}/data", headers=auth_headers)
    assert data.json()["total"] == 2


async def test_roles_limit_who_can_run(client, auth_headers, test_tenant, api):
    other = await _person(test_tenant["tenant_id"], "Viewer")
    insider = await _person(test_tenant["tenant_id"], "Insider")
    q = await _query(client, auth_headers, role_ids=[insider["role_id"]])
    assert (await client.post(f"/data-queries/{q['id']}/run", headers=other["headers"], json={})).status_code == 403
    assert (await client.post("/datasets/query", headers=other["headers"], json={"source_type": "query", "source_id": q["id"]})).status_code == 403
    assert (await client.post(f"/data-queries/{q['id']}/run", headers=insider["headers"], json={})).status_code == 200


async def test_failures_are_reported(client, auth_headers, monkeypatch):
    monkeypatch.setattr(data_queries, "_check_host", lambda url: None)
    monkeypatch.setattr(data_queries, "_http_call", lambda *a: (500, b"boom"))
    q = await _query(client, auth_headers, parameters=[{"name": "q", "required": True}])
    missing = await client.post(f"/data-queries/{q['id']}/run", headers=auth_headers, json={})
    assert missing.status_code == 422 and "required" in missing.json()["detail"]
    failed = await client.post(f"/data-queries/{q['id']}/run", headers=auth_headers, json={"params": {"q": "x"}})
    assert failed.status_code == 422 and "500" in failed.json()["detail"]
    bad = await client.post("/data-queries", headers=auth_headers, json={"name": "No SQL", "kind": "sql", "definition": {"sql": "select 1"}})
    assert bad.status_code == 422  # needs a Database connector


async def test_choice_options_from_a_query(client, auth_headers, api):
    q = await _query(client, auth_headers)
    form = await _form(
        client,
        auth_headers,
        [{"name": "country", "type": "dropdown", "label": "Country", "options_query_id": q["id"], "options_value_field": "code", "options_label_field": "name"}],
    )
    assert (await _submit(client, auth_headers, form, {"country": "JP"})).status_code == 201
    bad = await _submit(client, auth_headers, form, {"country": "XX"})
    assert bad.status_code == 422 and "available options" in bad.text


def test_private_and_metadata_addresses_are_refused(monkeypatch):
    with pytest.raises(data_queries.DataQueryError):
        data_queries._check_host("http://169.254.169.254/latest/meta-data")
    with pytest.raises(data_queries.DataQueryError):
        data_queries._check_host("file:///etc/passwd")
    monkeypatch.setattr(settings, "data_query_allow_private_hosts", False)
    with pytest.raises(data_queries.DataQueryError):
        data_queries._check_host("http://127.0.0.1:8000/")
    monkeypatch.setattr(settings, "data_query_allow_private_hosts", True)
    data_queries._check_host("http://127.0.0.1:8000/")


def test_extract_rows():
    assert data_queries.extract_rows([1, 2], None) == [{"value": 1}, {"value": 2}]
    assert data_queries.extract_rows({"a": {"b": [{"x": {"y": 1}}]}}, "a.b") == [{"x.y": 1}]
    with pytest.raises(data_queries.DataQueryError):
        data_queries.extract_rows({"a": 1}, "missing")


async def test_choice_option_filters_apply_to_query_rows(client, auth_headers, api):
    q = await _query(client, auth_headers)
    form = await _form(
        client,
        auth_headers,
        [{
            "name": "country", "type": "dropdown", "label": "Country", "options_query_id": q["id"], "options_value_field": "code",
            "option_filters": [{"field": "region.name", "operator": "equals", "value": "Asia"}],
        }],
    )
    assert (await _submit(client, auth_headers, form, {"country": "JP"})).status_code == 201
    assert (await _submit(client, auth_headers, form, {"country": "FR"})).status_code == 422  # filtered out
