"""
Unit tests for the provider-neutral cloud resource capability contract.

Covers:
  - Vocabulary completeness: all nine action keys and three capability states.
  - Reason code completeness.
  - CloudCapability validation (valid/invalid state, reason, messages).
  - CloudResource construction and kind validation.
  - CloudListing complete/incomplete semantics.
  - Connection defaults, item overrides, and merge behavior.
  - FakeAdapter: proves capability discovery invokes no put/delete/move/rename.
"""
from __future__ import annotations

import uuid
from datetime import datetime, timezone
from typing import Optional
from unittest.mock import AsyncMock

import pytest

from storage.cloud_base import (
    ACTIONS,
    CAPABILITY_STATES,
    KNOWN_REASONS,
    STATE_SUPPORTED,
    STATE_UNSUPPORTED,
    STATE_TEMPORARILY_UNAVAILABLE,
    REASON_PROVIDER_UNSUPPORTED,
    REASON_INSUFFICIENT_SCOPE,
    REASON_READ_ONLY,
    REASON_REAUTH_REQUIRED,
    REASON_OFFLINE,
    REASON_ITEM_RESTRICTED,
    CloudCapability,
    CloudResource,
    CloudListing,
    CloudResourceAdapter,
)


# ── Vocabulary completeness ───────────────────────────────────────────────────

def test_actions_contains_all_nine():
    expected = {
        "browse", "open", "preview", "upload", "create_folder",
        "rename", "move", "delete", "change_tracking",
    }
    assert ACTIONS == expected


def test_exactly_three_capability_states():
    assert CAPABILITY_STATES == {
        STATE_SUPPORTED,
        STATE_UNSUPPORTED,
        STATE_TEMPORARILY_UNAVAILABLE,
    }


def test_known_reasons_all_present():
    expected = {
        REASON_PROVIDER_UNSUPPORTED,
        REASON_INSUFFICIENT_SCOPE,
        REASON_READ_ONLY,
        REASON_REAUTH_REQUIRED,
        REASON_OFFLINE,
        REASON_ITEM_RESTRICTED,
    }
    assert KNOWN_REASONS == expected


# ── CloudCapability validation ────────────────────────────────────────────────

def test_supported_capability_no_reason_or_message():
    cap = CloudCapability(action="browse", state=STATE_SUPPORTED)
    assert cap.reason is None
    assert cap.message is None


def test_supported_capability_with_reason_raises():
    with pytest.raises(ValueError, match="reason and message must be None"):
        CloudCapability(action="browse", state=STATE_SUPPORTED, reason=REASON_READ_ONLY)


def test_supported_capability_with_message_raises():
    with pytest.raises(ValueError, match="reason and message must be None"):
        CloudCapability(action="browse", state=STATE_SUPPORTED, message="some msg")


def test_unsupported_capability_preserves_reason_and_message():
    cap = CloudCapability(
        action="rename",
        state=STATE_UNSUPPORTED,
        reason=REASON_PROVIDER_UNSUPPORTED,
        message="Rename is not supported by this server.",
    )
    assert cap.reason == REASON_PROVIDER_UNSUPPORTED
    assert cap.message == "Rename is not supported by this server."


def test_temporarily_unavailable_with_remedy_message():
    cap = CloudCapability(
        action="upload",
        state=STATE_TEMPORARILY_UNAVAILABLE,
        reason=REASON_INSUFFICIENT_SCOPE,
        message="Reconnect with write access to enable uploads.",
    )
    assert cap.state == STATE_TEMPORARILY_UNAVAILABLE
    assert cap.message == "Reconnect with write access to enable uploads."


def test_unknown_state_raises():
    with pytest.raises(ValueError, match="Unknown capability state"):
        CloudCapability(action="browse", state="kinda_works")


def test_unknown_action_raises():
    with pytest.raises(ValueError, match="Unknown action"):
        CloudCapability(action="fly", state=STATE_SUPPORTED)


def test_unknown_reason_raises():
    with pytest.raises(ValueError, match="Unknown reason code"):
        CloudCapability(action="delete", state=STATE_UNSUPPORTED, reason="bad_reason")


def test_reason_none_for_unsupported_is_valid():
    # reason is optional even for non-supported states
    cap = CloudCapability(action="delete", state=STATE_UNSUPPORTED)
    assert cap.reason is None


# ── CloudResource ─────────────────────────────────────────────────────────────

def _make_resource(**kwargs) -> CloudResource:
    defaults = dict(
        id=uuid.uuid4(),
        provider_item_id="provider-abc",
        connection_id=uuid.uuid4(),
        user_id=uuid.uuid4(),
        name="My Document.pdf",
        kind="file",
    )
    defaults.update(kwargs)
    return CloudResource(**defaults)


def test_resource_file_kind():
    r = _make_resource(kind="file")
    assert r.kind == "file"


def test_resource_folder_kind():
    r = _make_resource(kind="folder")
    assert r.kind == "folder"


def test_resource_invalid_kind_raises():
    with pytest.raises(ValueError, match="kind must be"):
        _make_resource(kind="symlink")


def test_resource_optional_fields_default_none():
    r = _make_resource()
    assert r.parent_ref is None
    assert r.content_type is None
    assert r.size is None
    assert r.modified_at is None
    assert r.etag is None
    assert r.capabilities == {}


def test_resource_with_full_metadata():
    now = datetime.now(timezone.utc)
    cap = CloudCapability(action="delete", state=STATE_UNSUPPORTED, reason=REASON_ITEM_RESTRICTED)
    r = _make_resource(
        parent_ref="parent-001",
        content_type="application/pdf",
        size=102400,
        modified_at=now,
        etag="etag-v42",
        capabilities={"delete": cap},
    )
    assert r.size == 102400
    assert r.etag == "etag-v42"
    assert r.capabilities["delete"] is cap


def test_resource_capability_invalid_type_raises():
    with pytest.raises(TypeError):
        _make_resource(capabilities={"delete": "not-a-capability"})


def test_resource_is_immutable():
    r = _make_resource()
    with pytest.raises(Exception):  # dataclass frozen=True raises FrozenInstanceError
        r.name = "changed"  # type: ignore[misc]


# ── CloudListing ──────────────────────────────────────────────────────────────

def test_listing_complete_default():
    listing = CloudListing(items=[])
    assert listing.complete is True


def test_listing_items_coerced_to_tuple():
    r = _make_resource()
    listing = CloudListing(items=[r])
    assert isinstance(listing.items, tuple)
    assert listing.items[0] is r


def test_listing_incomplete_retains_rows():
    r = _make_resource()
    listing = CloudListing(items=[r], complete=False, next_page_token="tok-abc")
    assert listing.complete is False
    assert listing.next_page_token == "tok-abc"


# ── Merge behavior ────────────────────────────────────────────────────────────

class _FakeAdapter(CloudResourceAdapter):
    """Minimal test double that records which methods were called."""

    def __init__(self):
        self._calls: list[str] = []

    async def list_folder(self, connection_id, user_id, parent_ref=None, page_token=None):
        self._calls.append("list_folder")
        return CloudListing(items=[], complete=True)

    async def get_capabilities(self, connection_id, user_id):
        self._calls.append("get_capabilities")
        return {
            action: CloudCapability(action=action, state=STATE_SUPPORTED)
            for action in ACTIONS
        }

    # Explicitly absent: any method that could mutate provider state.
    # Python raises AttributeError naturally if called — confirmed below.


def test_fake_adapter_has_no_mutation_methods():
    """Prove the Phase 12 interface defines no put/delete/move/rename methods."""
    adapter = _FakeAdapter()
    mutation_methods = ["put_object", "delete_object", "rename_item", "move_item", "create_folder_remote"]
    for method_name in mutation_methods:
        assert not hasattr(adapter, method_name), (
            f"Phase 12 adapter must not expose mutation method {method_name!r}"
        )


@pytest.mark.asyncio
async def test_fake_adapter_list_folder_invokes_no_mutation():
    adapter = _FakeAdapter()
    cid = uuid.uuid4()
    uid = uuid.uuid4()
    listing = await adapter.list_folder(cid, uid)
    assert isinstance(listing, CloudListing)
    assert "list_folder" in adapter._calls
    # Only list_folder was called, no mutation methods
    assert adapter._calls == ["list_folder"]


@pytest.mark.asyncio
async def test_fake_adapter_get_capabilities_covers_all_actions():
    adapter = _FakeAdapter()
    cid = uuid.uuid4()
    uid = uuid.uuid4()
    caps = await adapter.get_capabilities(cid, uid)
    for action in ACTIONS:
        assert action in caps, f"Missing capability for action {action!r}"
        assert isinstance(caps[action], CloudCapability)


def test_merge_item_override_takes_precedence():
    adapter = _FakeAdapter()
    connection_caps = {
        action: CloudCapability(action=action, state=STATE_SUPPORTED)
        for action in ACTIONS
    }
    item_override = {
        "delete": CloudCapability(
            action="delete",
            state=STATE_UNSUPPORTED,
            reason=REASON_ITEM_RESTRICTED,
            message="This item cannot be deleted.",
        )
    }
    merged = adapter.merge_item_capabilities(connection_caps, item_override)
    assert merged["delete"].state == STATE_UNSUPPORTED
    assert merged["delete"].reason == REASON_ITEM_RESTRICTED
    # Non-overridden actions fall back to connection caps
    assert merged["browse"].state == STATE_SUPPORTED


def test_merge_covers_all_actions():
    adapter = _FakeAdapter()
    merged = adapter.merge_item_capabilities({}, {})
    for action in ACTIONS:
        assert action in merged


def test_merge_empty_item_caps_uses_connection_defaults():
    adapter = _FakeAdapter()
    connection_caps = {
        action: CloudCapability(action=action, state=STATE_SUPPORTED)
        for action in ACTIONS
    }
    merged = adapter.merge_item_capabilities(connection_caps, {})
    for action in ACTIONS:
        assert merged[action].state == STATE_SUPPORTED


def test_exact_reason_and_message_preserved_through_merge():
    adapter = _FakeAdapter()
    cap = CloudCapability(
        action="rename",
        state=STATE_TEMPORARILY_UNAVAILABLE,
        reason=REASON_REAUTH_REQUIRED,
        message="Re-authenticate to rename files.",
    )
    merged = adapter.merge_item_capabilities(
        {action: CloudCapability(action=action, state=STATE_SUPPORTED) for action in ACTIONS},
        {"rename": cap},
    )
    result = merged["rename"]
    assert result.reason == REASON_REAUTH_REQUIRED
    assert result.message == "Re-authenticate to rename files."
