"""Tests for certisigma_census.key_rotation — encryption key rotation."""

from __future__ import annotations

import pytest
from unittest.mock import MagicMock, patch

from certisigma_census.key_rotation import (
    rotate_metadata_key,
    _validate_hex_key,
    RotationResult,
)

VALID_KEY_A = "aa" * 32
VALID_KEY_B = "bb" * 32


class TestValidateHexKey:
    def test_valid(self):
        _validate_hex_key(VALID_KEY_A, "test")

    def test_wrong_length(self):
        with pytest.raises(ValueError, match="64 hex"):
            _validate_hex_key("aabb", "test")

    def test_not_hex(self):
        with pytest.raises(ValueError, match="64 hex"):
            _validate_hex_key("zz" * 32, "test")

    def test_rejects_whitespace_padded_key(self):
        """Keys with whitespace rejected — int(x,16) accepts leading/trailing spaces."""
        key = " " + "a" * 62 + " "
        assert len(key) == 64
        with pytest.raises(ValueError, match="64 hex"):
            _validate_hex_key(key, "test")

    def test_rejects_underscore_key(self):
        """Keys with underscores rejected — int(x,16) accepts them in Python 3.6+."""
        key = "a_" + "a" * 62
        with pytest.raises(ValueError):
            _validate_hex_key(key, "test")


class TestRotateMetadataKey:
    @patch("certisigma.CertiSigmaClient")
    @patch("certisigma.crypto.is_encrypted", return_value=True)
    @patch("certisigma.crypto.rotate_key")
    def test_rotates_metadata(self, mock_rotate, mock_is_enc, MockClient):
        client = MockClient.return_value
        meta = MagicMock()
        meta.extra_data = {"ciphertext": "ct", "nonce": "n", "v": 1}
        client.get_metadata.return_value = meta
        mock_rotate.return_value = {"ciphertext": "new_ct", "nonce": "new_n", "v": 1}
        client.get_tags.return_value = []

        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is True
        assert result.rotated_metadata is True
        assert result.rotated_tags == 0
        client.update_metadata.assert_called_once()

    @patch("certisigma.CertiSigmaClient")
    @patch("certisigma.crypto.is_encrypted", return_value=True)
    @patch("certisigma.crypto.rotate_key")
    def test_rotates_tags(self, mock_rotate, mock_is_enc, MockClient):
        client = MockClient.return_value
        meta = MagicMock()
        meta.extra_data = {"ciphertext": "ct", "nonce": "n", "v": 1}
        client.get_metadata.return_value = meta

        tag = MagicMock()
        tag.key = "secret"
        tag.client_encrypted = True
        tag.value_enc = "enc_val"
        tag.value_nonce = "enc_nonce"
        client.get_tags.return_value = [tag]

        mock_rotate.return_value = {"ciphertext": "new", "nonce": "new_n", "v": 1}

        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is True
        assert result.rotated_metadata is True
        assert result.rotated_tags == 1

    @patch("certisigma.CertiSigmaClient")
    @patch("certisigma.crypto.is_encrypted", return_value=False)
    def test_no_encrypted_data(self, mock_is_enc, MockClient):
        client = MockClient.return_value
        meta = MagicMock()
        meta.extra_data = {"note": "plain"}
        client.get_metadata.return_value = meta
        client.get_tags.return_value = []

        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is True
        assert result.rotated_metadata is False
        assert result.rotated_tags == 0

    def test_same_keys_rejected(self):
        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_A)
        assert result.success is False
        assert "identical" in result.error

    @patch("certisigma.CertiSigmaClient")
    def test_api_error(self, MockClient):
        from certisigma import CertiSigmaError
        MockClient.return_value.get_metadata.side_effect = CertiSigmaError("fail")
        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is False
        assert "Metadata rotation failed" in result.error

    @patch("certisigma.CertiSigmaClient")
    @patch("certisigma.crypto.is_encrypted", return_value=True)
    @patch("certisigma.crypto.rotate_key", side_effect=ValueError("bad key"))
    def test_crypto_error(self, mock_rotate, mock_is_enc, MockClient):
        client = MockClient.return_value
        meta = MagicMock()
        meta.extra_data = {"ciphertext": "ct", "nonce": "n", "v": 1}
        client.get_metadata.return_value = meta

        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is False
        assert "crypto error" in result.error

    @patch("certisigma.CertiSigmaClient")
    @patch("certisigma.crypto.is_encrypted", return_value=True)
    @patch("certisigma.crypto.rotate_key")
    def test_partial_failure_metadata_ok_tags_fail(self, mock_rotate, mock_is_enc, MockClient):
        from certisigma import CertiSigmaError
        client = MockClient.return_value
        meta = MagicMock()
        meta.extra_data = {"ciphertext": "ct", "nonce": "n", "v": 1}
        client.get_metadata.return_value = meta
        mock_rotate.return_value = {"ciphertext": "new", "nonce": "nn", "v": 1}

        tag = MagicMock()
        tag.key = "secret"
        tag.client_encrypted = True
        tag.value_enc = "enc"
        tag.value_nonce = "nonce"
        client.get_tags.return_value = [tag]
        client.put_tags.side_effect = CertiSigmaError("tag update failed")

        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is False
        assert result.rotated_metadata is True
        assert "Tag rotation failed" in result.error
        assert "Do NOT discard the old key" in result.error

    @patch("certisigma.CertiSigmaClient")
    @patch("certisigma.crypto.is_encrypted", return_value=True)
    @patch("certisigma.crypto.rotate_key")
    def test_put_tags_sends_list_format(self, mock_rotate, mock_is_enc, MockClient):
        """Verify put_tags receives list[dict] with value_enc/value_nonce fields."""
        client = MockClient.return_value
        meta = MagicMock()
        meta.extra_data = {"ciphertext": "ct", "nonce": "n", "v": 1}
        client.get_metadata.return_value = meta
        mock_rotate.return_value = {"ciphertext": "new_ct", "nonce": "new_n", "v": 1}

        tag = MagicMock()
        tag.key = "secret"
        tag.client_encrypted = True
        tag.value_enc = "enc"
        tag.value_nonce = "nonce"
        client.get_tags.return_value = [tag]

        result = rotate_metadata_key("att_1", VALID_KEY_A, VALID_KEY_B, api_key="cs_test")
        assert result.success is True
        call_args = client.put_tags.call_args
        sent_tags = call_args[1]["tags"] if "tags" in call_args[1] else call_args[0][1]
        assert isinstance(sent_tags, list)
        assert sent_tags[0]["key"] == "secret"
        assert sent_tags[0]["value_enc"] == "new_ct"
        assert sent_tags[0]["value_nonce"] == "new_n"
        assert sent_tags[0]["client_encrypted"] is True
