import json
import tempfile
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4

import httpx
import pytest
from src.domain.codegen.legacy_data_export import (
    build_live_export_plan,
    discover_entity_keys,
    export_live_data,
    preview_origin_for_project,
    save_export,
    validate_export_plan,
)


def plan():
    return {
        "version": 1,
        "project_id": str(uuid4()),
        "origin": "https://app.example",
        "collections": [
            {
                "key": "users",
                "path": "/api/admin/users",
                "pagination": "page",
                "params": {"status": "all"},
            }
        ],
    }


def page(rows, total, number=1, limit=1):
    return {
        "success": True,
        "data": rows,
        "pagination": {"total": total, "page": number, "limit": limit},
    }


@pytest.mark.asyncio
async def test_all_pages_and_fields_are_preserved_and_verified():
    spec = plan()
    first = {"id": "old-1", "email": "owner@example.com", "custom": {"team": "sales"}}
    second = {"id": "old-2", "is_active": False, "created_at": "2025-01-01"}
    calls = []

    def respond(request):
        calls.append(request)
        number = int(request.url.params["page"])
        return httpx.Response(200, json=page([first if number == 1 else second], 2, number))

    async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
        result = await export_live_data(spec, spec["project_id"], client=client)
    assert result["collections"]["users"]["rows"] == [first, second]
    assert len(calls) == 4
    assert result["coverage"] == "listed_endpoints_only"
    assert len(result["sha256"]) == 64


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "responses",
    [
        [page([{"id": "a"}], 2), page([{"id": "a"}], 2, 2)],
        [page([{"id": "a"}], 2), page([], 2, 2)],
        [page([{"id": "a"}], 2), page([{"id": "b"}], 3, 2)],
        [page([{"id": "a", "name": "before"}], 1), page([{"id": "a", "name": "after"}], 1)],
        [{"success": True, "data": []}],
    ],
)
async def test_partial_or_unstable_exports_fail(responses):
    spec = plan()
    async with httpx.AsyncClient(
        transport=httpx.MockTransport(lambda _: httpx.Response(200, json=responses.pop(0)))
    ) as client:
        with pytest.raises(ValueError):
            await export_live_data(spec, spec["project_id"], client=client)


@pytest.mark.asyncio
async def test_http_error_does_not_leak_response_or_authorization():
    spec = plan()
    async with httpx.AsyncClient(
        transport=httpx.MockTransport(lambda _: httpx.Response(503, text="secret-response"))
    ) as client:
        with pytest.raises(ValueError, match="HTTP 503") as error:
            await export_live_data(spec, spec["project_id"], client=client)
    assert "secret-response" not in str(error.value)


def test_plan_cannot_export_a_different_tenant_or_redirect_origin():
    spec = plan()
    with pytest.raises(ValueError, match="different project"):
        validate_export_plan(spec, str(uuid4()))
    spec["collections"][0]["path"] = "//other.example/api/users"
    with pytest.raises(ValueError, match="literal"):
        validate_export_plan(spec, spec["project_id"])


def test_export_never_overwrites_and_stays_outside_git():
    with tempfile.TemporaryDirectory() as raw:
        tmp_path = Path(raw)
        snapshot = {"project_id": str(uuid4()), "collections": {}}
        first = save_export(snapshot, str(tmp_path))
        second = save_export(snapshot, str(tmp_path))
        assert first != second
        assert json.loads(first.read_text()) == snapshot
        (tmp_path / ".git").mkdir()
        with pytest.raises(ValueError, match="outside Git"):
            save_export(snapshot, str(tmp_path / "exports"))


def test_user_import_preserves_every_field_and_rejects_unmapped_data():
    from src.domain.codegen.legacy_data_export import _digest, load_user_export

    with tempfile.TemporaryDirectory() as raw:
        row = {
            "id": "old-1",
            "email": "staff@example.com",
            "role": "admin",
            "name": "Staff",
            "is_active": False,
            "team_id": "old-team",
        }
        snapshot = {
            "version": 1,
            "project_id": str(uuid4()),
            "collections": {"users": {"count": 1, "rows": [row]}},
        }
        snapshot["sha256"] = _digest(snapshot)
        path = save_export(snapshot, raw)
        users = load_user_export(str(path), snapshot["project_id"])
        assert users[0].legacy_data == row
        snapshot.pop("sha256")
        snapshot["collections"]["orders"] = {"count": 0, "rows": []}
        snapshot["sha256"] = _digest(snapshot)
        path = save_export(snapshot, raw)
        with pytest.raises(ValueError, match="mappings"):
            load_user_export(str(path), snapshot["project_id"])


def test_mapped_record_collections_are_imported_by_entity():
    from src.domain.codegen.legacy_data_export import _digest, load_record_export, load_user_export

    project_id = str(uuid4())
    users_row = {
        "id": "old-1",
        "email": "staff@example.com",
        "role": "admin",
        "name": "Staff",
        "is_active": True,
    }
    student = {"id": "stu-1", "first_name": "Ada", "email": "ada@example.com"}
    snapshot = {
        "version": 1,
        "project_id": project_id,
        "collections": {
            "users": {"count": 1, "rows": [users_row], "kind": "users"},
            "students": {
                "count": 1,
                "rows": [student],
                "kind": "records",
                "entity": "students",
            },
        },
    }
    snapshot["sha256"] = _digest(snapshot)
    with tempfile.TemporaryDirectory() as raw:
        path = save_export(snapshot, raw)
        assert load_user_export(str(path), project_id)[0].email == "staff@example.com"
        assert load_record_export(str(path), project_id)["students"] == [student]


@pytest.mark.asyncio
async def test_record_query_pages_are_exported_with_entity_mapping():
    spec = {
        "version": 1,
        "project_id": str(uuid4()),
        "origin": "https://app.example",
        "collections": [
            {
                "key": "students",
                "path": "/api/records/students/query",
                "method": "POST",
                "pagination": "offset",
                "entity": "students",
            }
        ],
    }
    first = {"id": "stu-1", "first_name": "Ada"}
    second = {"id": "stu-2", "first_name": "Alan"}
    calls = []

    def respond(request):
        calls.append(json.loads(request.content))
        offset = json.loads(request.content)["offset"]
        row = first if offset == 0 else second
        return httpx.Response(200, json={"records": [row], "total": 2})

    async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
        result = await export_live_data(spec, spec["project_id"], client=client)
    assert result["collections"]["students"]["rows"] == [first, second]
    assert result["collections"]["students"]["entity"] == "students"
    assert calls[0] == {"limit": 100, "offset": 0}


@pytest.mark.asyncio
async def test_optional_missing_record_collection_is_empty():
    spec = {
        "version": 1,
        "project_id": str(uuid4()),
        "origin": "https://app.example",
        "collections": [
            {
                "key": "students",
                "path": "/api/records/students/query",
                "method": "POST",
                "pagination": "offset",
                "entity": "students",
                "optional": True,
            }
        ],
    }

    async with httpx.AsyncClient(
        transport=httpx.MockTransport(lambda _: httpx.Response(404, json={"error": "missing"}))
    ) as client:
        result = await export_live_data(spec, spec["project_id"], client=client)
    assert result["collections"]["students"]["rows"] == []
    assert result["collections"]["students"]["count"] == 0


@pytest.mark.asyncio
async def test_optional_unconfigured_record_collection_is_empty():
    spec = {
        "version": 1,
        "project_id": str(uuid4()),
        "origin": "https://app.example",
        "collections": [
            {
                "key": "universities",
                "path": "/api/records/universities/query",
                "method": "POST",
                "pagination": "offset",
                "entity": "universities",
                "optional": True,
            }
        ],
    }

    async with httpx.AsyncClient(
        transport=httpx.MockTransport(
            lambda _: httpx.Response(503, json={"message": "Platform backend is not configured"})
        )
    ) as client:
        result = await export_live_data(spec, spec["project_id"], client=client)
    assert result["collections"]["universities"]["rows"] == []


@pytest.mark.asyncio
async def test_optional_404_after_first_page_still_fails():
    spec = {
        "version": 1,
        "project_id": str(uuid4()),
        "origin": "https://app.example",
        "collections": [
            {
                "key": "students",
                "path": "/api/records/students/query",
                "method": "POST",
                "pagination": "offset",
                "entity": "students",
                "optional": True,
            }
        ],
    }
    calls = {"n": 0}

    def respond(_request):
        calls["n"] += 1
        if calls["n"] == 1:
            return httpx.Response(200, json={"records": [{"id": "stu-1"}], "total": 2})
        return httpx.Response(404, json={"error": "missing"})

    async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
        with pytest.raises(ValueError, match="HTTP 404"):
            await export_live_data(spec, spec["project_id"], client=client)


def test_user_export_maps_custom_staff_roles():
    from src.domain.codegen.legacy_data_export import _digest, load_user_export

    row = {
        "id": "usr-counselor",
        "email": "counselor@example.com",
        "role": "counselor",
        "name": "Casey",
        "is_active": True,
    }
    snapshot = {
        "version": 1,
        "project_id": str(uuid4()),
        "collections": {"users": {"count": 1, "rows": [row]}},
    }
    snapshot["sha256"] = _digest(snapshot)
    with tempfile.TemporaryDirectory() as raw:
        path = save_export(snapshot, raw)
        users = load_user_export(str(path), snapshot["project_id"])
    assert users[0].role == "admin"
    assert users[0].legacy_data == row


def test_auto_export_plan_uses_entities_and_preview_hostname():
    project_id = str(uuid4())
    project = SimpleNamespace(
        preview_hostname="prev-p1q9l8dk.p2hsapp.com",
        project_ref="p1q9l8dk",
    )
    with tempfile.TemporaryDirectory() as raw:
        root = Path(raw)
        (root / "p2hs").mkdir()
        (root / "p2hs" / "entities.json").write_text(
            json.dumps(
                {
                    "entities": [
                        {"slug": "students"},
                        {"slug": "universities"},
                        {"slug": "applications"},
                    ]
                }
            ),
            encoding="utf-8",
        )
        (root / "server/api/records/[entity]").mkdir(parents=True)
        keys = discover_entity_keys(str(root))
        plan = build_live_export_plan(
            project_id, preview_origin_for_project(project), str(root)
        )
    assert keys == ["universities", "students", "applications"]
    assert plan["origin"] == "https://prev-p1q9l8dk.p2hsapp.com"
    assert plan["collections"][0]["key"] == "users"
    assert plan["collections"][0]["skip_unconfigured"] is True
    assert [item["entity"] for item in plan["collections"][1:]] == keys
    assert all(item.get("optional") for item in plan["collections"][1:])


@pytest.mark.asyncio
async def test_unconfigured_cloud_users_adapter_does_not_block_record_export():
    spec = {
        "version": 1,
        "project_id": str(uuid4()),
        "origin": "https://app.example",
        "collections": [
            {
                "key": "users",
                "path": "/api/admin/users",
                "pagination": "page",
                "skip_unconfigured": True,
            },
            {
                "key": "students",
                "path": "/api/records/students/query",
                "method": "POST",
                "pagination": "offset",
                "entity": "students",
                "optional": True,
            },
        ],
    }
    student = {"id": "stu-1", "first_name": "Ada"}

    def respond(request):
        if request.url.path.endswith("/users"):
            return httpx.Response(
                503,
                json={"statusCode": 503, "message": "Platform backend is not configured"},
            )
        return httpx.Response(200, json={"records": [student], "total": 1})

    async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
        result = await export_live_data(spec, spec["project_id"], client=client)
    assert result["collections"]["users"]["rows"] == []
    assert result["collections"]["students"]["rows"] == [student]
