# grantflow/tests/test_integration_tenants.py

import datetime
import json

from fastapi.testclient import TestClient

import grantflow.api.routes.ingest as ingest_routes
from grantflow.api import runtime_state
from grantflow.api.app import app
from grantflow.api.tenant_store import _clear_registry_for_tests

client = TestClient(app)


def setup_function():
    _clear_registry_for_tests()
    runtime_state.INGEST_AUDIT_STORE.clear()


def test_provision_tenant_creates_and_returns_record():
    response = client.post(
        "/tenants",
        json={
            "tenant_id": "Acme NGO",
            "display_name": "Acme Non-Governmental Organization",
            "allowed_donors": ["usaid", "eu"],
            "quota_jobs_per_day": 50,
            "quota_ingest_per_day": 200,
        },
    )
    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "registered"
    tenant = body["tenant"]
    # tenant_id should be normalized (lowercased, spaces → underscores)
    assert tenant["tenant_id"] == "acme_ngo"
    assert tenant["display_name"] == "Acme Non-Governmental Organization"
    assert set(tenant["allowed_donors"]) == {"usaid", "eu"}
    assert tenant["quota_jobs_per_day"] == 50
    assert tenant["quota_ingest_per_day"] == 200
    assert tenant["created_at"]
    assert tenant["updated_at"]


def test_provision_tenant_is_idempotent_and_updates():
    client.post("/tenants", json={"tenant_id": "tenant_beta", "quota_jobs_per_day": 10})
    response = client.post(
        "/tenants",
        json={"tenant_id": "tenant_beta", "display_name": "Beta Team", "quota_jobs_per_day": 99},
    )
    assert response.status_code == 200
    tenant = response.json()["tenant"]
    assert tenant["display_name"] == "Beta Team"
    assert tenant["quota_jobs_per_day"] == 99
    # created_at must not change on update
    r1 = client.post("/tenants", json={"tenant_id": "tenant_gamma"}).json()["tenant"]
    r2 = client.post("/tenants", json={"tenant_id": "tenant_gamma", "quota_jobs_per_day": 5}).json()["tenant"]
    assert r1["created_at"] == r2["created_at"]


def test_provision_tenant_rejects_empty_tenant_id():
    response = client.post("/tenants", json={"tenant_id": ""})
    assert response.status_code == 400
    detail = response.json()["detail"]
    assert detail["code"] == "missing_tenant_id"
    assert detail["retryable"] is False


def test_get_tenants_list_returns_all_registered():
    client.post("/tenants", json={"tenant_id": "org_a", "quota_jobs_per_day": 20})
    client.post("/tenants", json={"tenant_id": "org_b", "quota_jobs_per_day": 30})

    response = client.get("/tenants")
    assert response.status_code == 200
    body = response.json()
    assert body["count"] == 2
    ids = {t["tenant_id"] for t in body["tenants"]}
    assert ids == {"org_a", "org_b"}


def test_get_tenant_detail_returns_config_and_usage():
    client.post(
        "/tenants",
        json={
            "tenant_id": "tenant_delta",
            "quota_jobs_per_day": 50,
            "quota_ingest_per_day": 100,
        },
    )

    response = client.get("/tenants/tenant_delta")
    assert response.status_code == 200
    body = response.json()
    assert body["tenant"]["tenant_id"] == "tenant_delta"
    assert body["tenant"]["quota_jobs_per_day"] == 50

    usage = body["usage"]
    assert usage["period"] == "today"
    assert "jobs_total" in usage
    assert "jobs_done" in usage
    assert "ingest_total" in usage
    assert "chunks_ingested_total" in usage
    assert usage["quota_jobs_per_day"] == 50
    assert usage["quota_ingest_per_day"] == 100
    assert "quota_jobs_remaining" in usage
    assert "quota_ingest_remaining" in usage


def test_get_tenant_detail_returns_404_for_unregistered():
    response = client.get("/tenants/nonexistent_xyz_999")
    assert response.status_code == 404
    detail = response.json()["detail"]
    assert detail["code"] == "tenant_not_found"
    assert detail["retryable"] is False


def test_get_tenant_usage_endpoint_returns_metrics():
    client.post("/tenants", json={"tenant_id": "tenant_epsilon", "quota_jobs_per_day": 10})

    response = client.get("/tenants/tenant_epsilon/usage")
    assert response.status_code == 200
    body = response.json()
    assert body["tenant_id"] == "tenant_epsilon"
    assert body["registered"] is True
    assert body["period"] == "today"
    assert isinstance(body["jobs_total"], int)
    assert isinstance(body["ingest_total"], int)
    assert "quota_jobs_remaining" in body


def test_get_tenant_usage_works_for_unregistered_tenant():
    """Usage endpoint works even for tenants not in the registry (0 quota = unlimited)."""
    response = client.get("/tenants/some_unknown_tenant/usage")
    assert response.status_code == 200
    body = response.json()
    assert body["registered"] is False
    assert body["jobs_total"] == 0
    assert body["quota_jobs_per_day"] == 0
    assert "quota_jobs_remaining" not in body  # unlimited has no remaining count


def test_usage_counts_ingest_events_for_tenant(monkeypatch):
    def fake_ingest(pdf_path: str, namespace: str, metadata=None):
        return {"namespace": namespace, "source": pdf_path, "chunks_ingested": 5, "stats": {}}

    monkeypatch.setattr(ingest_routes, "ingest_pdf_to_namespace", fake_ingest)

    client.post("/tenants", json={"tenant_id": "tenant_zeta", "quota_ingest_per_day": 20})

    client.post(
        "/ingest",
        data={
            "donor_id": "usaid",
            "tenant_id": "tenant_zeta",
            "metadata_json": json.dumps({"doc_family": "donor_policy"}),
        },
        files={"file": ("doc1.pdf", b"%PDF-1.4 a", "application/pdf")},
    )
    client.post(
        "/ingest",
        data={
            "donor_id": "usaid",
            "tenant_id": "tenant_zeta",
            "metadata_json": json.dumps({"doc_family": "country_context"}),
        },
        files={"file": ("doc2.pdf", b"%PDF-1.4 b", "application/pdf")},
    )

    response = client.get("/tenants/tenant_zeta/usage")
    assert response.status_code == 200
    body = response.json()
    assert body["ingest_total"] == 2
    assert body["chunks_ingested_total"] == 10
    assert body["quota_ingest_remaining"] == 18  # 20 - 2


def test_quota_remaining_is_absent_when_quota_is_zero():
    """quota_jobs_remaining must not appear when quota_jobs_per_day == 0 (unlimited)."""
    client.post("/tenants", json={"tenant_id": "tenant_unlimited", "quota_jobs_per_day": 0})

    response = client.get("/tenants/tenant_unlimited/usage")
    assert response.status_code == 200
    body = response.json()
    assert "quota_jobs_remaining" not in body


def test_provision_tenant_requires_api_key_when_configured(monkeypatch):
    monkeypatch.setenv("GRANTFLOW_API_KEY", "test-secret")

    response = client.post("/tenants", json={"tenant_id": "blocked_tenant"})
    assert response.status_code == 401

    response = client.post(
        "/tenants",
        json={"tenant_id": "allowed_tenant"},
        headers={"X-API-Key": "test-secret"},
    )
    assert response.status_code == 200


def test_get_tenants_list_requires_api_key_when_configured(monkeypatch):
    monkeypatch.setenv("GRANTFLOW_API_KEY", "test-secret")

    response = client.get("/tenants")
    assert response.status_code == 401

    response = client.get("/tenants", headers={"X-API-Key": "test-secret"})
    assert response.status_code == 200


def test_get_tenant_detail_requires_api_key_when_configured(monkeypatch):
    client.post("/tenants", json={"tenant_id": "tenant_auth_check"})
    monkeypatch.setenv("GRANTFLOW_API_KEY", "test-secret")

    response = client.get("/tenants/tenant_auth_check")
    assert response.status_code == 401

    response = client.get("/tenants/tenant_auth_check", headers={"X-API-Key": "test-secret"})
    assert response.status_code == 200


def test_get_tenant_usage_requires_api_key_when_configured(monkeypatch):
    monkeypatch.setenv("GRANTFLOW_API_KEY", "test-secret")

    response = client.get("/tenants/any_tenant/usage")
    assert response.status_code == 401

    response = client.get("/tenants/any_tenant/usage", headers={"X-API-Key": "test-secret"})
    assert response.status_code == 200


def test_usage_counts_job_statuses_correctly():
    """jobs_done, jobs_error, jobs_pending are counted from JOB_STORE for the tenant."""
    now_ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
    tenant_id = "tenant_job_status_test"

    def _make_job(job_id: str, status: str) -> dict:
        return {
            "status": status,
            "client_metadata": {"tenant_id": tenant_id},
            "job_events": [{"ts": now_ts, "event": "created"}],
        }

    runtime_state.JOB_STORE.set("tsj-done-1", _make_job("tsj-done-1", "done"))
    runtime_state.JOB_STORE.set("tsj-done-2", _make_job("tsj-done-2", "done"))
    runtime_state.JOB_STORE.set("tsj-error-1", _make_job("tsj-error-1", "error"))
    runtime_state.JOB_STORE.set("tsj-pending-1", _make_job("tsj-pending-1", "running"))

    client.post("/tenants", json={"tenant_id": tenant_id})
    response = client.get(f"/tenants/{tenant_id}/usage")
    assert response.status_code == 200
    body = response.json()

    assert body["jobs_total"] == 4
    assert body["jobs_done"] == 2
    assert body["jobs_error"] == 1
    assert body["jobs_pending"] == 1


def test_quota_jobs_remaining_is_zero_when_exhausted():
    """quota_jobs_remaining must be 0 (not negative) when jobs exceed quota."""
    now_ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
    tenant_id = "tenant_quota_exhausted"

    for i in range(5):
        runtime_state.JOB_STORE.set(
            f"qex-job-{i}",
            {
                "status": "done",
                "client_metadata": {"tenant_id": tenant_id},
                "job_events": [{"ts": now_ts, "event": "created"}],
            },
        )

    client.post("/tenants", json={"tenant_id": tenant_id, "quota_jobs_per_day": 3})
    response = client.get(f"/tenants/{tenant_id}/usage")
    assert response.status_code == 200
    body = response.json()

    assert body["jobs_total"] == 5
    assert body["quota_jobs_remaining"] == 0


def test_provision_tenant_stores_and_returns_allowed_donors():
    """allowed_donors are preserved through register and returned on GET."""
    client.post(
        "/tenants",
        json={"tenant_id": "tenant_donors", "allowed_donors": ["usaid", "worldbank", "eu"]},
    )

    response = client.get("/tenants/tenant_donors")
    assert response.status_code == 200
    tenant = response.json()["tenant"]
    assert set(tenant["allowed_donors"]) == {"usaid", "worldbank", "eu"}


def test_get_tenant_detail_404_is_agent_error_format():
    """404 from GET /tenants/{id} must follow the agent error contract."""
    response = client.get("/tenants/totally_nonexistent_tenant_xyz")
    assert response.status_code == 404
    detail = response.json()["detail"]
    assert detail["code"] == "tenant_not_found"
    assert detail["retryable"] is False
    assert "next_action" in detail


def test_usage_period_date_utc_is_present():
    """Usage response must include period_date_utc for auditability."""
    client.post("/tenants", json={"tenant_id": "tenant_period_check"})
    response = client.get("/tenants/tenant_period_check/usage")
    assert response.status_code == 200
    body = response.json()
    assert "period_date_utc" in body
    assert body["period"] == "today"


# --- _check_tenant_quota unit tests ---


def test_check_tenant_quota_noop_when_flag_disabled(monkeypatch):
    """_check_tenant_quota must do nothing when GRANTFLOW_TENANT_QUOTA_HARD_BLOCK is not set."""
    from grantflow.api.tenant import _check_tenant_quota

    monkeypatch.delenv("GRANTFLOW_TENANT_QUOTA_HARD_BLOCK", raising=False)
    _check_tenant_quota("any_tenant", quota_type="jobs")


def test_check_tenant_quota_noop_when_tenant_has_no_config(monkeypatch):
    """_check_tenant_quota must do nothing when the tenant has no registered quota."""
    from grantflow.api.tenant import _check_tenant_quota

    monkeypatch.setenv("GRANTFLOW_TENANT_QUOTA_HARD_BLOCK", "true")
    _check_tenant_quota("nonexistent_tenant_xyz", quota_type="jobs")


def test_check_tenant_quota_jobs_blocks_at_limit(monkeypatch):
    """quota_type='jobs' must raise 429 when today's job count >= quota."""
    from grantflow.api.tenant import _check_tenant_quota

    monkeypatch.setenv("GRANTFLOW_TENANT_QUOTA_HARD_BLOCK", "true")
    tenant_id = "tenant_quota_block_jobs"
    client.post("/tenants", json={"tenant_id": tenant_id, "quota_jobs_per_day": 2})

    today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
    for i in range(2):
        runtime_state.JOB_STORE.set(
            f"qblock-job-{i}",
            {
                "status": "done",
                "client_metadata": {"tenant_id": tenant_id},
                "state": {"created_at": f"{today}T10:0{i}:00+00:00"},
            },
        )

    import pytest
    from fastapi import HTTPException

    with pytest.raises(HTTPException) as exc_info:
        _check_tenant_quota(tenant_id, quota_type="jobs")
    assert exc_info.value.status_code == 429
    assert exc_info.value.detail["code"] == "tenant_quota_exceeded"
    assert exc_info.value.detail["retryable"] is False


def test_check_tenant_quota_jobs_allows_under_limit(monkeypatch):
    """quota_type='jobs' must not raise when today's job count < quota."""
    from grantflow.api.tenant import _check_tenant_quota

    monkeypatch.setenv("GRANTFLOW_TENANT_QUOTA_HARD_BLOCK", "true")
    tenant_id = "tenant_quota_under_jobs"
    client.post("/tenants", json={"tenant_id": tenant_id, "quota_jobs_per_day": 5})

    today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
    runtime_state.JOB_STORE.set(
        "qunder-job-0",
        {
            "status": "done",
            "client_metadata": {"tenant_id": tenant_id},
            "state": {"created_at": f"{today}T09:00:00+00:00"},
        },
    )

    _check_tenant_quota(tenant_id, quota_type="jobs")


def test_check_tenant_quota_ingest_blocks_at_limit(monkeypatch):
    """quota_type='ingest' must raise 429 when today's ingest count >= quota."""
    from grantflow.api.tenant import _check_tenant_quota

    monkeypatch.setenv("GRANTFLOW_TENANT_QUOTA_HARD_BLOCK", "true")
    tenant_id = "tenant_quota_block_ingest"
    client.post("/tenants", json={"tenant_id": tenant_id, "quota_ingest_per_day": 1})

    today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
    runtime_state.INGEST_AUDIT_STORE.append(
        {
            "event_id": "qingest-evt-1",
            "ts": f"{today}T08:00:00+00:00",
            "donor_id": "usaid",
            "namespace": f"{tenant_id}/usaid_ads201",
            "filename": "doc.pdf",
            "content_type": "application/pdf",
            "metadata": {"tenant_id": tenant_id},
            "result": {"chunks_ingested": 2},
        }
    )

    import pytest
    from fastapi import HTTPException

    with pytest.raises(HTTPException) as exc_info:
        _check_tenant_quota(tenant_id, quota_type="ingest")
    assert exc_info.value.status_code == 429
    assert exc_info.value.detail["code"] == "tenant_quota_exceeded"
