"""End-to-end tests for the sync Space client against a real backend."""

import os
import uuid

import pytest

import json

from reeeductio import Space, Ed25519KeyPair, generate_keypair, NotFoundError

pytestmark = pytest.mark.e2e


@pytest.fixture
def space(fresh_keypair, symmetric_root, base_url):
    """Create a Space client. Backend auto-creates spaces on first use."""
    space_id = fresh_keypair.to_space_id()
    with Space(
        space_id=space_id,
        member_id=fresh_keypair.to_user_id(),
        private_key=fresh_keypair.private_key,
        symmetric_root=symmetric_root,
        base_url=base_url,
    ) as s:
        yield s


@pytest.fixture
def space_with_user_key(fresh_keypair, symmetric_root, user_symmetric_key, base_url):
    """Create a Space client with a user-private symmetric key."""
    space_id = fresh_keypair.to_space_id()
    with Space(
        space_id=space_id,
        member_id=fresh_keypair.to_user_id(),
        private_key=fresh_keypair.private_key,
        symmetric_root=symmetric_root,
        user_symmetric_key=user_symmetric_key,
        base_url=base_url,
    ) as s:
        yield s


class TestAuthentication:
    def test_authenticate_returns_token(self, space):
        token = space.authenticate()
        assert isinstance(token, str)
        assert len(token) > 0

    def test_auto_authenticate_on_request(self, fresh_keypair, symmetric_root, base_url):
        """Space with auto_authenticate=True should auth transparently."""
        space_id = fresh_keypair.to_space_id()
        with Space(
            space_id=space_id,
            member_id=fresh_keypair.to_user_id(),
            private_key=fresh_keypair.private_key,
            symmetric_root=symmetric_root,
            base_url=base_url,
            auto_authenticate=True,
        ) as s:
            # This triggers auto-auth internally
            msgs = s.get_messages("nonexistent-topic")
            assert isinstance(msgs, list)


class TestPlaintextMessages:
    def test_post_and_retrieve_message(self, space):
        topic = f"test-topic-{uuid.uuid4().hex[:8]}"
        content = b"hello world"

        created = space.post_message(topic, "test.message", content)
        assert created.message_hash is not None
        assert created.server_timestamp > 0

        msg = space.get_message(topic, created.message_hash)
        assert msg.message_hash == created.message_hash
        assert msg.type == "test.message"

    def test_get_messages_list(self, space):
        topic = f"test-topic-{uuid.uuid4().hex[:8]}"

        space.post_message(topic, "msg", b"first")
        space.post_message(topic, "msg", b"second")

        msgs = space.get_messages(topic)
        assert len(msgs) >= 2

    def test_get_messages_empty_topic(self, space):
        msgs = space.get_messages(f"empty-{uuid.uuid4().hex[:8]}")
        assert msgs == []

    def test_get_message_not_found(self, space):
        topic = f"test-topic-{uuid.uuid4().hex[:8]}"
        # Post one message so the topic exists
        space.post_message(topic, "msg", b"data")
        with pytest.raises(NotFoundError):
            space.get_message(topic, "M" + "A" * 43)


class TestEncryptedMessages:
    def test_post_encrypted_and_retrieve(self, space):
        """Post an encrypted message and verify we can read it back."""
        from reeeductio.crypto import encrypt_aes_gcm, decrypt_aes_gcm, derive_key
        import base64

        topic = f"enc-topic-{uuid.uuid4().hex[:8]}"
        plaintext = b"secret message content"

        # Derive a topic key the same way the SDK would
        topic_key = derive_key(space.message_key, f"topic key | {topic}")
        encrypted = encrypt_aes_gcm(plaintext, topic_key)

        created = space.post_message(topic, "encrypted.msg", encrypted)
        assert created.message_hash is not None

        msg = space.get_message(topic, created.message_hash)
        # Decode the data from base64 and decrypt
        encrypted_data = base64.b64decode(msg.data)
        decrypted = decrypt_aes_gcm(encrypted_data, topic_key)
        assert decrypted == plaintext


class TestPlaintextState:
    def test_set_and_get_state(self, space):
        path = f"test/state/{uuid.uuid4().hex[:8]}"
        space.set_plaintext_state(path, "hello state")
        result = space.get_plaintext_state(path)
        assert result == "hello state"

    def test_overwrite_state(self, space):
        path = f"test/state/{uuid.uuid4().hex[:8]}"
        space.set_plaintext_state(path, "v1")
        space.set_plaintext_state(path, "v2")
        result = space.get_plaintext_state(path)
        assert result == "v2"


class TestEncryptedState:
    def test_set_and_get_encrypted_state(self, space):
        path = f"test/enc-state/{uuid.uuid4().hex[:8]}"
        space.set_encrypted_state(path, "secret state value")
        result = space.get_encrypted_state(path)
        assert result == "secret state value"

    def test_encrypted_state_overwrite(self, space):
        path = f"test/enc-state/{uuid.uuid4().hex[:8]}"
        space.set_encrypted_state(path, "old secret")
        space.set_encrypted_state(path, "new secret")
        result = space.get_encrypted_state(path)
        assert result == "new secret"

    def test_custom_key_round_trips(self, space):
        """set/get_encrypted_state with an explicit key should round-trip."""
        from reeeductio.crypto import derive_key
        custom_key = derive_key(os.urandom(32), "test-custom-state-key")
        path = f"test/enc-state/{uuid.uuid4().hex[:8]}"
        space.set_encrypted_state(path, "state with custom key", key=custom_key)
        result = space.get_encrypted_state(path, key=custom_key)
        assert result == "state with custom key"


class TestStateHistory:
    def test_state_history(self, space):
        path1 = f"test/hist/{uuid.uuid4().hex[:8]}"
        path2 = f"test/hist/{uuid.uuid4().hex[:8]}"

        space.set_plaintext_state(path1, "a")
        space.set_plaintext_state(path2, "b")
        space.set_plaintext_state(path1, "c")

        history = space.get_state_history()
        assert len(history) >= 3


class TestPlaintextBlobs:
    def test_upload_and_download(self, space):
        data = b"blob content here"
        created = space.upload_plaintext_blob(data)
        assert created.blob_id is not None

        downloaded = space.download_plaintext_blob(created.blob_id)
        assert downloaded == data

    def test_delete_blob(self, space):
        data = b"to be deleted"
        created = space.upload_plaintext_blob(data)
        space.delete_blob(created.blob_id)

    def test_upload_large_blob(self, space):
        data = os.urandom(1024 * 100)  # 100 KB
        created = space.upload_plaintext_blob(data)
        downloaded = space.download_plaintext_blob(created.blob_id)
        assert downloaded == data


class TestEncryptedBlobs:
    def test_encrypt_upload_download_decrypt(self, space):
        plaintext = b"encrypted blob content"
        created = space.encrypt_and_upload_blob(plaintext)
        assert created.blob_id is not None

        decrypted = space.download_and_decrypt_blob(created.blob_id, created.key)
        assert decrypted == plaintext

    def test_encrypted_blob_not_readable_as_plaintext(self, space):
        """Downloading encrypted blob as plaintext should NOT match original."""
        plaintext = b"this should be encrypted"
        created = space.encrypt_and_upload_blob(plaintext)
        raw = space.download_plaintext_blob(created.blob_id)
        assert raw != plaintext


class TestPlaintextKVData:
    def test_set_and_get(self, space):
        path = f"test/data/{uuid.uuid4().hex[:8]}"
        data = b"kv data value"
        ts = space.set_plaintext_data(path, data)
        assert ts > 0

        result = space.get_plaintext_data(path)
        assert result == data

    def test_overwrite(self, space):
        path = f"test/data/{uuid.uuid4().hex[:8]}"
        space.set_plaintext_data(path, b"v1")
        space.set_plaintext_data(path, b"v2")
        result = space.get_plaintext_data(path)
        assert result == b"v2"


class TestEncryptedKVData:
    def test_set_and_get_encrypted(self, space):
        path = f"test/enc-data/{uuid.uuid4().hex[:8]}"
        data = b"secret kv data"
        ts = space.set_encrypted_data(path, data)
        assert ts > 0

        result = space.get_encrypted_data(path)
        assert result == data

    def test_encrypted_data_not_readable_as_plaintext(self, space):
        """Reading encrypted data as plaintext should NOT match original."""
        path = f"test/enc-data/{uuid.uuid4().hex[:8]}"
        data = b"this is secret"
        space.set_encrypted_data(path, data)
        raw = space.get_plaintext_data(path)
        assert raw != data

    def test_custom_key_round_trips(self, space):
        """set/get_encrypted_data with an explicit key should round-trip."""
        from reeeductio.crypto import derive_key
        custom_key = derive_key(os.urandom(32), "test-custom-key")
        path = f"test/enc-data/{uuid.uuid4().hex[:8]}"
        data = b"data encrypted with custom key"
        space.set_encrypted_data(path, data, key=custom_key)
        result = space.get_encrypted_data(path, key=custom_key)
        assert result == data


class TestCreateTool:
    def test_create_tool_stores_entry(self, space):
        """create_tool should store tool data at auth/tools/{tool_id}."""
        keypair = generate_keypair()
        tool_id = keypair.to_tool_id()

        result = space.create_tool(tool_id, "Test tool")
        assert result.message_hash is not None
        assert result.message_hash.startswith("M")

        tool_data = space.get_plaintext_state(f"auth/tools/{tool_id}")
        tool = json.loads(tool_data)
        assert tool["tool_id"] == tool_id
        assert tool["description"] == "Test tool"

    def test_create_tool_without_description(self, space):
        """create_tool should work without a description."""
        keypair = generate_keypair()
        tool_id = keypair.to_tool_id()

        result = space.create_tool(tool_id)
        assert result.message_hash is not None

        tool_data = space.get_plaintext_state(f"auth/tools/{tool_id}")
        tool = json.loads(tool_data)
        assert tool["tool_id"] == tool_id
        assert "description" not in tool


class TestCreateInvitation:
    def test_create_invitation_returns_keypair(self, space):
        """create_invitation should return an Ed25519KeyPair."""
        keypair = space.create_invitation()
        assert isinstance(keypair, Ed25519KeyPair)
        assert len(keypair.private_key) == 32
        assert len(keypair.public_key) == 32

    def test_create_invitation_creates_tool_entry(self, space):
        """create_invitation should create a tool entry for the keypair."""
        keypair = space.create_invitation(description="Test invitation")
        tool_id = keypair.to_tool_id()

        tool_data = space.get_plaintext_state(f"auth/tools/{tool_id}")
        tool = json.loads(tool_data)
        assert tool["tool_id"] == tool_id
        assert tool["description"] == "Test invitation"

    def test_create_invitation_grants_create_user_capability(self, space):
        """create_invitation should grant the tool permission to create users."""
        keypair = space.create_invitation()
        tool_id = keypair.to_tool_id()

        cap_data = space.get_plaintext_state(
            f"auth/tools/{tool_id}/rights/can_create_user"
        )
        cap = json.loads(cap_data)
        assert cap["op"] == "create"
        assert cap["path"] == "state/auth/users/{any}"

    def test_create_invitation_grants_assign_user_role_capability(self, space):
        """create_invitation should grant the tool permission to assign the user role."""
        keypair = space.create_invitation()
        tool_id = keypair.to_tool_id()

        cap_data = space.get_plaintext_state(
            f"auth/tools/{tool_id}/rights/can_grant_user_role"
        )
        cap = json.loads(cap_data)
        assert cap["op"] == "create"
        assert cap["path"] == "state/auth/users/{any}/roles/user"

    def test_create_multiple_invitations_are_independent(self, space):
        """Each create_invitation call should produce a unique tool."""
        keypair1 = space.create_invitation()
        keypair2 = space.create_invitation()

        assert keypair1.to_tool_id() != keypair2.to_tool_id()

        # Both tools should exist independently
        tool1 = json.loads(
            space.get_plaintext_state(f"auth/tools/{keypair1.to_tool_id()}")
        )
        tool2 = json.loads(
            space.get_plaintext_state(f"auth/tools/{keypair2.to_tool_id()}")
        )
        assert tool1["tool_id"] == keypair1.to_tool_id()
        assert tool2["tool_id"] == keypair2.to_tool_id()

    def test_create_and_use_invitation(self, space):
        # Create a new invitation tool
        tool_keypair = space.create_invitation()
        # Generate the keypair for a new user
        new_user_keypair = generate_keypair()
        new_user_id = new_user_keypair.to_user_id()
        # Connect to the space as the tool
        tool_space = Space(space.space_id, tool_keypair.to_tool_id(), tool_keypair.private_key, space.symmetric_root, space.base_url)
        # Use the tool to add the user in the space
        tool_space.add_user(new_user_id)
        # Use the tool to add the user to the "user group"
        tool_space.assign_role_to_user(new_user_id, "user")
        # Connect to the space as the new user
        user_space = Space(space.space_id, new_user_keypair.to_user_id(), new_user_keypair.private_key, space.symmetric_root, space.base_url)

class TestUserPrivateKVData:
    def test_set_and_get_user_data(self, space_with_user_key):
        """Round-trip through set/get_encrypted_user_data."""
        path = f"notes/{uuid.uuid4().hex[:8]}"
        data = b"private user data"
        ts = space_with_user_key.set_encrypted_user_data(path, data)
        assert ts > 0
        result = space_with_user_key.get_encrypted_user_data(path)
        assert result == data

    def test_user_data_is_encrypted(self, space_with_user_key):
        """Reading user-private data as plaintext should NOT match the original."""
        path = f"notes/{uuid.uuid4().hex[:8]}"
        data = b"private user data"
        space_with_user_key.set_encrypted_user_data(path, data)
        member_id = space_with_user_key.member_id
        raw = space_with_user_key.get_plaintext_data(f"user/{member_id}/{path}")
        assert raw != data

    def test_user_data_stored_under_member_namespace(self, space_with_user_key):
        """Data should be readable at the full namespaced path."""
        path = f"notes/{uuid.uuid4().hex[:8]}"
        data = b"namespaced private data"
        space_with_user_key.set_encrypted_user_data(path, data)
        member_id = space_with_user_key.member_id
        result = space_with_user_key.get_encrypted_data(
            f"user/{member_id}/{path}",
            key=space_with_user_key.user_data_key,
        )
        assert result == data

    def test_raises_without_user_key(self, space):
        """Methods should raise ValueError when user_symmetric_key was not provided."""
        with pytest.raises(ValueError, match="user_symmetric_key"):
            space.set_encrypted_user_data("notes/test", b"data")
        with pytest.raises(ValueError, match="user_symmetric_key"):
            space.get_encrypted_user_data("notes/test")
