# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""Unit tests for the Bitwarden Password Manager secrets provider."""

from unittest.mock import MagicMock, patch

import requests
from django.test import TestCase
from nautobot.extras.secrets.exceptions import SecretParametersError, SecretProviderError, SecretValueNotFoundError

from nautobot_bitwarden_password_manager_secrets.secrets import BitwardenPasswordManagerSecretsProvider

# Sample Bitwarden vault item response
SAMPLE_ITEM = {
    "success": True,
    "data": {
        "object": "item",
        "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
        "type": 1,
        "name": "example.com",
        "notes": "Some secret notes",
        "fields": [
            {"name": "API_KEY", "value": "sk-abc123", "type": 0},
            {"name": "DB_PASSWORD", "value": "hunter2", "type": 1},
        ],
        "login": {
            "username": "admin@example.com",
            "password": "super-secret-password",
            "totp": "otpauth://totp/Example:admin@example.com?secret=JBSWY3DPEHPK3PXP",
            "uris": [
                {"uri": "https://example.com/login", "match": None},
            ],
        },
    },
}


def _make_secret(params):
    """Create a mock Secret object with the given rendered parameters."""
    secret = MagicMock()
    secret.rendered_parameters.return_value = params
    return secret


def _make_response(json_data, status_code=200):
    """Create a mock requests.Response."""
    response = MagicMock()
    response.status_code = status_code
    response.json.return_value = json_data
    response.text = str(json_data)
    return response


class TestParametersForm(TestCase):
    """Tests for the ParametersForm validation."""

    def test_valid_login_field(self):
        form = BitwardenPasswordManagerSecretsProvider.ParametersForm(
            data={"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"}
        )
        self.assertTrue(form.is_valid(), form.errors)

    def test_valid_custom_field(self):
        form = BitwardenPasswordManagerSecretsProvider.ParametersForm(
            data={
                "item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                "secret_field": "custom_field",
                "custom_field_name": "API_KEY",
            }
        )
        self.assertTrue(form.is_valid(), form.errors)

    def test_custom_field_without_name_is_invalid(self):
        form = BitwardenPasswordManagerSecretsProvider.ParametersForm(
            data={
                "item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                "secret_field": "custom_field",
                "custom_field_name": "",
            }
        )
        self.assertFalse(form.is_valid())
        self.assertIn("custom_field_name", form.errors)

    def test_missing_item_id_is_invalid(self):
        form = BitwardenPasswordManagerSecretsProvider.ParametersForm(data={"item_id": "", "secret_field": "password"})
        self.assertFalse(form.is_valid())
        self.assertIn("item_id", form.errors)


class TestGetValueSuccess(TestCase):
    """Tests for successful secret value retrieval."""

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_get_username(self, mock_get):
        mock_get.return_value = _make_response(SAMPLE_ITEM)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "username"})

        result = BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertEqual(result, "admin@example.com")

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_get_password(self, mock_get):
        mock_get.return_value = _make_response(SAMPLE_ITEM)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"})

        result = BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertEqual(result, "super-secret-password")

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_get_totp(self, mock_get):
        mock_get.return_value = _make_response(SAMPLE_ITEM)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "totp"})

        result = BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertEqual(result, "otpauth://totp/Example:admin@example.com?secret=JBSWY3DPEHPK3PXP")

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_get_uri(self, mock_get):
        mock_get.return_value = _make_response(SAMPLE_ITEM)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "uri"})

        result = BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertEqual(result, "https://example.com/login")

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_get_notes(self, mock_get):
        mock_get.return_value = _make_response(SAMPLE_ITEM)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "notes"})

        result = BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertEqual(result, "Some secret notes")

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_get_custom_field(self, mock_get):
        mock_get.return_value = _make_response(SAMPLE_ITEM)
        secret = _make_secret(
            {
                "item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                "secret_field": "custom_field",
                "custom_field_name": "API_KEY",
            }
        )

        result = BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertEqual(result, "sk-abc123")


class TestGetValueErrors(TestCase):
    """Tests for error handling in secret value retrieval."""

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_connection_error(self, mock_get):
        mock_get.side_effect = requests.ConnectionError("Connection refused")
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"})

        with self.assertRaises(SecretProviderError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("Unable to connect", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_request_exception(self, mock_get):
        mock_get.side_effect = requests.RequestException("Timeout")
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"})

        with self.assertRaises(SecretProviderError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("Error communicating", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_http_error(self, mock_get):
        mock_get.return_value = _make_response({"message": "Not found"}, status_code=404)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"})

        with self.assertRaises(SecretProviderError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("HTTP 404", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_api_error_response(self, mock_get):
        mock_get.return_value = _make_response({"success": False, "message": "Vault is locked"})
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"})

        with self.assertRaises(SecretProviderError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("Vault is locked", str(ctx.exception))

    def test_missing_item_id(self):
        secret = _make_secret({"item_id": "", "secret_field": "password"})

        with self.assertRaises(SecretParametersError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("item_id", str(ctx.exception))

    def test_missing_secret_field(self):
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": ""})

        with self.assertRaises(SecretParametersError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("secret_field", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_not_a_login_item(self, mock_get):
        item = {"success": True, "data": {"type": 2, "name": "Secure Note", "notes": None, "login": None}}
        mock_get.return_value = _make_response(item)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "password"})

        with self.assertRaises(SecretValueNotFoundError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("not a Login type", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_no_uris(self, mock_get):
        item = {
            "success": True,
            "data": {"type": 1, "login": {"username": "user", "password": "pass", "totp": None, "uris": []}},
        }
        mock_get.return_value = _make_response(item)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "uri"})

        with self.assertRaises(SecretValueNotFoundError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("no URIs", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_field_value_is_none(self, mock_get):
        item = {
            "success": True,
            "data": {"type": 1, "login": {"username": None, "password": "pass", "totp": None, "uris": []}},
        }
        mock_get.return_value = _make_response(item)
        secret = _make_secret({"item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret_field": "username"})

        with self.assertRaises(SecretValueNotFoundError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("not set", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_custom_field_not_found(self, mock_get):
        item = {
            "success": True,
            "data": {"fields": [{"name": "OTHER", "value": "val", "type": 0}]},
        }
        mock_get.return_value = _make_response(item)
        secret = _make_secret(
            {
                "item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                "secret_field": "custom_field",
                "custom_field_name": "MISSING",
            }
        )

        with self.assertRaises(SecretValueNotFoundError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("MISSING", str(ctx.exception))

    @patch("nautobot_bitwarden_password_manager_secrets.secrets.requests.get")
    def test_no_custom_fields_at_all(self, mock_get):
        item = {"success": True, "data": {"fields": None}}
        mock_get.return_value = _make_response(item)
        secret = _make_secret(
            {
                "item_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                "secret_field": "custom_field",
                "custom_field_name": "API_KEY",
            }
        )

        with self.assertRaises(SecretValueNotFoundError) as ctx:
            BitwardenPasswordManagerSecretsProvider.get_value_for_secret(secret)
        self.assertIn("no custom fields", str(ctx.exception))
