"""Tests for previously untested resource methods and error classes.

Covers:
- Assets.update()
- Assets.sync_filter()
- Schemas.update()
- Schemas.delete()
- ForbiddenError
- _build_create_payload / _build_update_payload helpers
"""

from __future__ import annotations

import warnings

import pytest
from unittest.mock import MagicMock, patch

from optropic import Optropic
from optropic.errors import ForbiddenError, OptropicError
from optropic.types import (
    CreateAssetParams,
    UpdateAssetParams,
    UpdateSchemaParams,
)
from optropic.resources.assets import _build_create_payload, _build_update_payload


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def client():
    return Optropic(api_key="optr_test_abcdefghijklmnopqrst")


@pytest.fixture
def mock_request(client):
    with patch.object(client, "_request") as m:
        yield m


@pytest.fixture
def mock_request_raw(client):
    with patch.object(client, "_request_raw") as m:
        yield m


# ---------------------------------------------------------------------------
# ForbiddenError
# ---------------------------------------------------------------------------


class TestForbiddenError:
    def test_default(self):
        err = ForbiddenError()
        assert isinstance(err, OptropicError)
        assert err.status_code == 403
        assert err.message == "Forbidden"

    def test_custom_message_and_code(self):
        err = ForbiddenError(message="No access to resource", code="INSUFFICIENT_PERMISSIONS")
        assert err.message == "No access to resource"
        assert err.code == "INSUFFICIENT_PERMISSIONS"
        assert err.status_code == 403

    def test_repr(self):
        err = ForbiddenError()
        assert "ForbiddenError" in repr(err)


# ---------------------------------------------------------------------------
# Assets.update()
# ---------------------------------------------------------------------------


class TestAssetsUpdate:
    def test_update_with_dataclass(self, client, mock_request):
        mock_request.return_value = {
            "id": "asset-1",
            "external_id": "NEW-SKU",
            "status": "active",
        }
        params = UpdateAssetParams(external_id="NEW-SKU")
        result = client.assets.update("asset-1", params)

        assert result["external_id"] == "NEW-SKU"
        call_args = mock_request.call_args
        assert call_args[0] == ("PATCH", "/assets/asset-1")
        body = call_args[1]["json"]
        assert body["external_id"] == "NEW-SKU"

    def test_update_with_kwargs(self, client, mock_request):
        mock_request.return_value = {"id": "asset-1", "metadata": {"batch": "B42"}}
        result = client.assets.update("asset-1", metadata={"batch": "B42"})

        call_args = mock_request.call_args
        body = call_args[1]["json"]
        assert body["metadata"] == {"batch": "B42"}

    def test_update_strips_none_values(self, client, mock_request):
        mock_request.return_value = {"id": "asset-1"}
        params = UpdateAssetParams(external_id="X", vertical=None)
        client.assets.update("asset-1", params)

        call_args = mock_request.call_args
        body = call_args[1]["json"]
        assert "vertical" not in body

    def test_update_product_name_deprecation(self, client, mock_request):
        mock_request.return_value = {"id": "asset-1"}
        params = UpdateAssetParams(product_name="Widget")

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            client.assets.update("asset-1", params)
            assert any("deprecated" in str(warning.message).lower() for warning in w)

        call_args = mock_request.call_args
        body = call_args[1]["json"]
        assert body["config"]["product_name"] == "Widget"


# ---------------------------------------------------------------------------
# Assets.sync_filter()
# ---------------------------------------------------------------------------


class TestAssetsSyncFilter:
    def test_sync_filter_with_multi_salts(self, client, mock_request_raw):
        import struct
        import time

        now = int(time.time())
        header = struct.pack(">BqIHI", 1, now, 5, 1, 64)
        bucket_data = b"\x00" * (64 * 4 * 2)
        signature = b"\x00" * 64
        filter_binary = header + bucket_data + signature

        response_mock = MagicMock()
        response_mock.content = filter_binary
        response_mock.headers = {
            "x-optropic-filter-salts": "t1:aabb,t2:ccdd",
        }
        mock_request_raw.return_value = response_mock

        result = client.assets.sync_filter()

        assert result.filter == filter_binary
        assert result.item_count == 5
        assert "t1" in result.salts
        assert "t2" in result.salts
        mock_request_raw.assert_called_once_with("GET", "/sync/revocation-filter")

    def test_sync_filter_with_single_salt(self, client, mock_request_raw):
        import struct
        import time

        now = int(time.time())
        header = struct.pack(">BqIHI", 1, now, 0, 1, 32)
        bucket_data = b"\x00" * (32 * 4 * 2)
        signature = b"\x00" * 64
        filter_binary = header + bucket_data + signature

        response_mock = MagicMock()
        response_mock.content = filter_binary
        response_mock.headers = {
            "x-optropic-filter-salts": "",
            "x-optropic-filter-salt": "deadbeef",
        }
        mock_request_raw.return_value = response_mock

        result = client.assets.sync_filter()

        assert "_self" in result.salts
        assert result.salts["_self"] == bytes.fromhex("deadbeef")

    def test_sync_filter_no_salts(self, client, mock_request_raw):
        import struct
        import time

        now = int(time.time())
        header = struct.pack(">BqIHI", 1, now, 0, 1, 32)
        bucket_data = b"\x00" * (32 * 4 * 2)
        signature = b"\x00" * 64

        response_mock = MagicMock()
        response_mock.content = header + bucket_data + signature
        response_mock.headers = {}
        mock_request_raw.return_value = response_mock

        result = client.assets.sync_filter()
        assert result.salts == {}


# ---------------------------------------------------------------------------
# Schemas.update()
# ---------------------------------------------------------------------------


class TestSchemasUpdate:
    def test_update(self, client, mock_request):
        mock_request.return_value = {
            "id": "schema-1",
            "tenantId": "tenant-1",
            "verticalId": "pharma",
            "version": "2.0",
            "metadataSchema": {"gtin": {"type": "string", "required": True}},
            "exportFormats": ["csv"],
            "description": "Updated schema",
            "isActive": True,
            "createdAt": "2026-01-01T00:00:00Z",
            "updatedAt": "2026-03-27T00:00:00Z",
        }
        params = UpdateSchemaParams(version="2.0", description="Updated schema")
        result = client.schemas.update("pharma", params)

        assert result.verticalId == "pharma"
        call_args = mock_request.call_args
        assert call_args[0] == ("PATCH", "/schemas/pharma")
        body = call_args[1]["json"]
        assert body["version"] == "2.0"
        assert body["description"] == "Updated schema"

    def test_update_strips_none(self, client, mock_request):
        mock_request.return_value = {
            "id": "schema-1",
            "tenantId": "tenant-1",
            "verticalId": "pharma",
            "version": "1.0",
            "metadataSchema": {},
            "exportFormats": [],
            "description": "",
            "isActive": True,
            "createdAt": "2026-01-01T00:00:00Z",
            "updatedAt": "2026-03-27T00:00:00Z",
        }
        params = UpdateSchemaParams(version="1.0", metadata_schema=None)
        client.schemas.update("pharma", params)

        call_args = mock_request.call_args
        body = call_args[1]["json"]
        assert "metadata_schema" not in body
        assert body["version"] == "1.0"


# ---------------------------------------------------------------------------
# Schemas.delete()
# ---------------------------------------------------------------------------


class TestSchemasDelete:
    def test_delete(self, client, mock_request):
        mock_request.return_value = {"vertical_id": "pharma", "status": "deactivated"}
        result = client.schemas.delete("pharma")

        assert result["status"] == "deactivated"
        mock_request.assert_called_once_with("DELETE", "/schemas/pharma")


# ---------------------------------------------------------------------------
# Payload builders
# ---------------------------------------------------------------------------


class TestBuildCreatePayload:
    def test_from_dataclass(self):
        params = CreateAssetParams(
            external_id="SKU-001",
            vertical="pharma",
            security_level="signed",
            config={"product_name": "Aspirin"},
        )
        payload = _build_create_payload(params)

        assert payload["external_id"] == "SKU-001"
        assert payload["vertical"] == "pharma"
        assert payload["security_level"] == "signed"
        assert payload["config"]["product_name"] == "Aspirin"

    def test_from_dict(self):
        params = {"external_id": "SKU-002", "config": {"batch": "B1"}}
        payload = _build_create_payload(params)

        assert payload["external_id"] == "SKU-002"
        assert payload["config"]["batch"] == "B1"

    def test_product_name_deprecation(self):
        params = CreateAssetParams(product_name="Widget")
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            payload = _build_create_payload(params)
            assert any("deprecated" in str(warning.message).lower() for warning in w)

        assert payload["config"]["product_name"] == "Widget"

    def test_strips_none_values(self):
        params = CreateAssetParams(external_id="X", vertical=None, metadata=None)
        payload = _build_create_payload(params)
        assert "vertical" not in payload
        assert "metadata" not in payload


class TestBuildUpdatePayload:
    def test_from_dataclass(self):
        params = UpdateAssetParams(external_id="NEW-SKU", metadata={"v": 2})
        payload = _build_update_payload(params)

        assert payload["external_id"] == "NEW-SKU"
        assert payload["metadata"]["v"] == 2

    def test_strips_none(self):
        params = UpdateAssetParams(external_id="X")
        payload = _build_update_payload(params)
        assert "vertical" not in payload

    def test_product_name_into_config(self):
        params = UpdateAssetParams(product_name="Widget 2.0")
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            payload = _build_update_payload(params)

        assert payload["config"]["product_name"] == "Widget 2.0"
