import pytest
from aioresponses import aioresponses
from pydantic import ValidationError

from mirage.accessor.onedrive import OneDriveConfig
from mirage.core.onedrive.client import (GraphError, drive_base,
                                         drive_ref_path, graph_get,
                                         graph_get_bytes, graph_list, headers,
                                         item_url, split_path)
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key


def test_split_path_strips_real_prefix():
    p = PathSpec(resource_path=mount_key("/od/a.txt", "/od"),
                 virtual="/od/a.txt",
                 directory="/od/a.txt")
    assert split_path(p) == ("/od", "a.txt")


def test_split_path_does_not_strip_sibling_prefix_match():
    # `/data` must not be stripped from the sibling `/database.txt`; the
    # boundary guard lives in mount_key, whose output split_path returns.
    p = PathSpec(resource_path=mount_key("/database.txt", "/data"),
                 virtual="/database.txt",
                 directory="/database.txt")
    assert split_path(p) == ("", "database.txt")


def test_headers_resolves_callable_token():
    h = headers(OneDriveConfig(access_token=lambda: "live-token"))
    assert h["Authorization"] == "Bearer live-token"


def _cfg(**kw) -> OneDriveConfig:
    return OneDriveConfig(access_token="tok", **kw)


def test_drive_base_defaults_to_me_drive():
    assert drive_base(_cfg()) == "https://graph.microsoft.com/v1.0/me/drive"


def test_drive_base_uses_drive_id():
    base = drive_base(_cfg(drive_id="b!abc"))
    assert base == "https://graph.microsoft.com/v1.0/drives/b!abc"


def test_drive_base_uses_site_default_drive():
    base = drive_base(_cfg(site_id="site123"))
    assert base == "https://graph.microsoft.com/v1.0/sites/site123/drive"


def test_drive_base_uses_group_drive():
    base = drive_base(_cfg(group_id="grp123"))
    assert base == "https://graph.microsoft.com/v1.0/groups/grp123/drive"


def test_drive_base_uses_user_drive():
    base = drive_base(_cfg(user_id="usr@example.com"))
    assert base == ("https://graph.microsoft.com/v1.0/users/"
                    "usr%40example.com/drive")


def test_drive_base_escapes_a_guest_upn():
    # A guest's UPN carries `#EXT#`. Interpolated raw, that `#` opens a
    # URL fragment, so Graph receives `/users/guest_contoso.com` and
    # answers 404 for a user that exists.
    base = drive_base(_cfg(user_id="guest_contoso.com#EXT#@fabrikam.com"))
    assert base == ("https://graph.microsoft.com/v1.0/users/"
                    "guest_contoso.com%23EXT%23%40fabrikam.com/drive")


def test_drive_base_leaves_a_drive_id_bang_alone():
    # `quote` would escape `!`, `encodeURIComponent` does not, and drive
    # ids start `b!`. The ref path built off this base goes into a JSON
    # body Graph reads literally, so the two languages must agree.
    assert "b!" in drive_base(_cfg(drive_id="b!abc-_x"))


def test_graph_base_url_replaces_the_service_root():
    # How a mount reaches a sovereign cloud, a private endpoint, or a
    # test server. The trailing slash must not survive into the URL.
    cfg = _cfg(graph_base_url="http://127.0.0.1:8080/v1.0/")
    assert drive_base(cfg) == "http://127.0.0.1:8080/v1.0/me/drive"


def test_drive_ref_path_survives_a_custom_base():
    # ref paths are Graph-root-relative, so they must be the base stripped
    # off, never a hardcoded host stripped off.
    cfg = _cfg(drive_id="b!abc", graph_base_url="http://127.0.0.1:8080/v1.0")
    assert drive_ref_path(cfg, "sub/dir") == "/drives/b!abc/root:/sub/dir"


def test_config_rejects_two_drive_targets():
    # A fixed precedence would silently address one and ignore the other.
    with pytest.raises(ValidationError, match="names 2 drives"):
        _cfg(drive_id="b!abc", site_id="site123")


def test_item_url_root_children():
    url = item_url(_cfg(), "/", action="/children")
    assert url == "https://graph.microsoft.com/v1.0/me/drive/root/children"


def test_item_url_nested_metadata_no_action():
    url = item_url(_cfg(), "/Docs/report.docx")
    assert url == (
        "https://graph.microsoft.com/v1.0/me/drive/root:/Docs/report.docx")


def test_item_url_nested_content():
    url = item_url(_cfg(), "/Docs/report.docx", action="/content")
    assert url == ("https://graph.microsoft.com/v1.0/me/drive"
                   "/root:/Docs/report.docx:/content")


def test_item_url_nested_children():
    url = item_url(_cfg(), "/Docs", action="/children")
    assert url == (
        "https://graph.microsoft.com/v1.0/me/drive/root:/Docs:/children")


def test_item_url_applies_key_prefix():
    url = item_url(_cfg(key_prefix="team/files"), "/a.txt", action="/content")
    assert url == ("https://graph.microsoft.com/v1.0/me/drive"
                   "/root:/team/files/a.txt:/content")


def test_item_url_quotes_spaces():
    url = item_url(_cfg(), "/My Folder/a b.txt")
    assert url == ("https://graph.microsoft.com/v1.0/me/drive"
                   "/root:/My%20Folder/a%20b.txt")


def test_headers_carry_bearer_token():
    h = headers(_cfg())
    assert h["Authorization"] == "Bearer tok"


_ROOT = "https://graph.microsoft.com/v1.0/me/drive/root"


@pytest.mark.asyncio
async def test_graph_get_returns_parsed_json():
    with aioresponses() as m:
        m.get(_ROOT, payload={"id": "01ABC", "name": "root"})
        result = await graph_get(_cfg(), _ROOT)
    assert result["id"] == "01ABC"


@pytest.mark.asyncio
async def test_graph_get_raises_grapherror_with_status():
    with aioresponses() as m:
        m.get(_ROOT,
              status=404,
              payload={"error": {
                  "code": "itemNotFound",
                  "message": "nope"
              }})
        with pytest.raises(GraphError) as exc:
            await graph_get(_cfg(), _ROOT)
    assert exc.value.status == 404
    assert exc.value.code == "itemNotFound"


@pytest.mark.asyncio
async def test_graph_list_follows_odata_nextlink():
    page2 = _ROOT + "/children?$skiptoken=abc"
    with aioresponses() as m:
        m.get(_ROOT + "/children",
              payload={
                  "value": [{
                      "id": "a"
                  }],
                  "@odata.nextLink": page2
              })
        m.get(page2, payload={"value": [{"id": "b"}]})
        items = await graph_list(_cfg(), _ROOT + "/children")
    assert [i["id"] for i in items] == ["a", "b"]


@pytest.mark.asyncio
async def test_graph_get_bytes_returns_raw_content():
    url = _ROOT + ":/a.txt:/content"
    with aioresponses() as m:
        m.get(url, body=b"hello bytes")
        data = await graph_get_bytes(_cfg(), url)
    assert data == b"hello bytes"


@pytest.mark.asyncio
async def test_request_retries_on_429_then_succeeds():
    with aioresponses() as m:
        m.get(_ROOT, status=429, headers={"Retry-After": "0"})
        m.get(_ROOT, payload={"id": "ok"})
        result = await graph_get(_cfg(), _ROOT)
    assert result["id"] == "ok"


@pytest.mark.asyncio
async def test_request_gives_up_after_max_retries():
    with aioresponses() as m:
        for _ in range(3):
            m.get(_ROOT, status=429, headers={"Retry-After": "0"})
        with pytest.raises(GraphError) as exc:
            await graph_get(_cfg(max_retries=2), _ROOT)
    assert exc.value.status == 429


@pytest.mark.asyncio
async def test_401_refreshes_callable_token_once_then_succeeds():
    calls = {"n": 0}

    def provider():
        calls["n"] += 1
        return "fresh" if calls["n"] > 1 else "stale"

    with aioresponses() as m:
        m.get(_ROOT,
              status=401,
              payload={"error": {
                  "code": "InvalidAuthenticationToken"
              }})
        m.get(_ROOT, payload={"id": "ok"})
        result = await graph_get(OneDriveConfig(access_token=provider), _ROOT)
    assert result["id"] == "ok"
    assert calls["n"] == 2


@pytest.mark.asyncio
async def test_401_with_static_token_does_not_retry():
    with aioresponses() as m:
        m.get(_ROOT, status=401, payload={"error": {"code": "x"}})
        with pytest.raises(GraphError) as exc:
            await graph_get(_cfg(), _ROOT)
    assert exc.value.status == 401
