"""Tests for certisigma_census.derived_lists — opaque third-party hash verification."""

from __future__ import annotations

import hashlib
import hmac as hmac_mod

import pytest
from unittest.mock import MagicMock, patch

from certisigma_census.derived_lists import (
    create_derived_list,
    list_derived_lists,
    get_derived_list_detail,
    derive_hashes,
    match_derived_list,
    revoke_derived_list,
    get_access_log,
    get_signature,
    DerivedListCreateResult,
    DerivedListInfo,
    DerivedListDetail,
    MatchResult,
    SignatureResult,
    AccessLogEntry,
)


class TestDeriveHashes:
    def test_hmac_correctness(self):
        key = "aa" * 32
        plain = ["abcd" * 16]
        result = derive_hashes(plain, key)
        expected = hmac_mod.new(
            bytes.fromhex(key),
            plain[0].encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()
        assert result == [expected]

    def test_multiple_hashes(self):
        key = "bb" * 32
        result = derive_hashes(["hash1", "hash2", "hash3"], key)
        assert len(result) == 3
        assert len(set(result)) == 3

    def test_invalid_key_too_short(self):
        with pytest.raises(ValueError, match="64 hex"):
            derive_hashes(["hash1"], "aabb")

    def test_invalid_key_not_hex(self):
        with pytest.raises(ValueError, match="64 hex"):
            derive_hashes(["hash1"], "zz" * 32)

    def test_rejects_whitespace_padded_key(self):
        """Keys with whitespace must be rejected even if len==64."""
        key_with_spaces = "a" * 62 + "  "
        assert len(key_with_spaces) == 64
        with pytest.raises(ValueError, match="64 hex"):
            derive_hashes(["hash1"], key_with_spaces)

    def test_rejects_key_with_internal_spaces(self):
        """Internal spaces that bytes.fromhex would strip."""
        key_with_spaces = "aa " * 21 + "a" * 1
        with pytest.raises(ValueError):
            derive_hashes(["hash1"], key_with_spaces)


class TestCreateDerivedList:
    @patch("certisigma.CertiSigmaClient")
    def test_success_with_hashes(self, MockClient):
        mock_result = MagicMock()
        mock_result.id = "dl_123"
        mock_result.list_key = "cc" * 32
        mock_result.item_count = 5
        mock_result.content_hash = "dd" * 32
        mock_result.signature = "sig"
        mock_result.label = "Q1"
        mock_result.expires_at = "2026-06-01"
        mock_result.warning = None
        MockClient.return_value.create_derived_list.return_value = mock_result

        result = create_derived_list(
            hashes=["aa" * 32, "bb" * 32],
            label="Q1",
            api_key="cs_test",
        )
        assert result.success is True
        assert result.list_id == "dl_123"
        assert result.list_key == "cc" * 32

    def test_no_hashes_no_filter(self):
        result = create_derived_list()
        assert result.success is False
        assert "Provide either" in result.error

    @patch("certisigma.CertiSigmaClient")
    def test_api_error(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.create_derived_list.side_effect = CertiSigmaError("fail")
        result = create_derived_list(hashes=["aa" * 32], api_key="cs_test")
        assert result.success is False

    @patch("certisigma.CertiSigmaClient")
    def test_with_tag_filter(self, MockClient):
        mock_result = MagicMock()
        mock_result.id = "dl_456"
        mock_result.list_key = "ee" * 32
        mock_result.item_count = 10
        mock_result.content_hash = "ff" * 32
        mock_result.signature = "sig2"
        mock_result.label = None
        mock_result.expires_at = None
        mock_result.warning = None
        MockClient.return_value.create_derived_list.return_value = mock_result

        result = create_derived_list(
            tag_filter={"and": [{"key": "dept", "value": "legal"}]},
            api_key="cs_test",
        )
        assert result.success is True


class TestListDerivedLists:
    @patch("certisigma.CertiSigmaClient")
    def test_returns_list(self, MockClient):
        item = MagicMock()
        item.id = "dl_1"
        item.item_count = 100
        item.label = "Test"
        item.expires_at = "2026-06-01"
        item.revoked_at = None
        item.created_at = "2026-03-01"
        item.active = True
        MockClient.return_value.list_derived_lists.return_value = [item]

        items = list_derived_lists(api_key="cs_test")
        assert len(items) == 1
        assert items[0].list_id == "dl_1"
        assert items[0].active is True


    @patch("certisigma.CertiSigmaClient")
    def test_api_error_returns_empty(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.list_derived_lists.side_effect = CertiSigmaError("fail")
        items = list_derived_lists(api_key="cs_test")
        assert items == []


class TestGetDerivedListDetail:
    @patch("certisigma.CertiSigmaClient")
    def test_returns_detail(self, MockClient):
        d = MagicMock()
        d.id = "dl_1"
        d.item_count = 50
        d.content_hash = "aa" * 32
        d.label = "Test"
        d.tag_filter = None
        d.expires_at = "2026-06-01"
        d.revoked_at = None
        d.created_at = "2026-03-01"
        d.active = True
        d.match_count = 5
        MockClient.return_value.get_derived_list.return_value = d

        detail = get_derived_list_detail("dl_1", api_key="cs_test")
        assert isinstance(detail, DerivedListDetail)
        assert detail.match_count == 5

    @patch("certisigma.CertiSigmaClient")
    def test_error_returns_create_result(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.get_derived_list.side_effect = CertiSigmaError("gone")
        result = get_derived_list_detail("dl_bad", api_key="cs_test")
        assert isinstance(result, DerivedListCreateResult)
        assert result.success is False


class TestMatchDerivedList:
    @patch("certisigma.CertiSigmaClient")
    def test_success(self, MockClient):
        mock_result = MagicMock()
        mock_result.matched = 2
        mock_result.unmatched = 3
        mock_result.total = 5
        mock_result.matches = [{"derived_hash": "h1"}, {"derived_hash": "h2"}]
        MockClient.return_value.match_derived_list.return_value = mock_result

        result = match_derived_list(
            "dl_1", "cc" * 32, ["hash1", "hash2", "hash3", "hash4", "hash5"],
            api_key="cs_test",
        )
        assert result.success is True
        assert result.matched == 2
        assert result.unmatched == 3

    def test_empty_hashes(self):
        result = match_derived_list("dl_1", "cc" * 32, [])
        assert result.success is False
        assert "No hashes" in result.error

    def test_invalid_list_key(self):
        result = match_derived_list("dl_1", "short", ["hash1"])
        assert result.success is False
        assert "Invalid list_key" in result.error

    def test_invalid_list_key_not_hex(self):
        result = match_derived_list("dl_1", "zz" * 32, ["hash1"])
        assert result.success is False
        assert "Invalid list_key" in result.error


class TestRevokeDerivedList:
    @patch("certisigma.CertiSigmaClient")
    def test_success(self, MockClient):
        MockClient.return_value.revoke_derived_list.return_value = None
        result = revoke_derived_list("dl_1", api_key="cs_test")
        assert result.success is True

    @patch("certisigma.CertiSigmaClient")
    def test_error(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.revoke_derived_list.side_effect = CertiSigmaError("fail")
        result = revoke_derived_list("dl_bad", api_key="cs_test")
        assert result.success is False


class TestGetAccessLog:
    @patch("certisigma.CertiSigmaClient")
    def test_returns_entries(self, MockClient):
        e = MagicMock()
        e.matched_count = 3
        e.total_submitted = 10
        e.ip_address = "1.2.3.4"
        e.created_at = "2026-03-20"
        MockClient.return_value.get_derived_list_access_log.return_value = [e]

        entries = get_access_log("dl_1", api_key="cs_test")
        assert len(entries) == 1
        assert entries[0].matched_count == 3
        assert entries[0].ip_address == "1.2.3.4"

    @patch("certisigma.CertiSigmaClient")
    def test_error_returns_empty(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.get_derived_list_access_log.side_effect = CertiSigmaError("fail")
        entries = get_access_log("dl_bad", api_key="cs_test")
        assert entries == []


class TestGetSignature:
    @patch("certisigma.CertiSigmaClient")
    def test_success(self, MockClient):
        sig = MagicMock()
        sig.list_id = "dl_1"
        sig.signature = "sig_ecdsa_hex"
        sig.signing_key_id = "key_01"
        sig.content_hash = "abcd" * 16
        sig.item_count = 42
        sig.created_at = "2026-03-18T10:00:00Z"
        sig.expires_at = "2026-06-18T10:00:00Z"
        MockClient.return_value.get_derived_list_signature.return_value = sig

        result = get_signature("dl_1")
        assert result.success is True
        assert result.signature == "sig_ecdsa_hex"
        assert result.signing_key_id == "key_01"
        assert result.item_count == 42

    @patch("certisigma.CertiSigmaClient")
    def test_error(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.get_derived_list_signature.side_effect = CertiSigmaError("not found")
        result = get_signature("dl_bad")
        assert result.success is False
        assert "not found" in result.error
