# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Tests for the debusine Cli Asset commands."""

import base64
import math
from typing import Any
from unittest import mock
from unittest.mock import MagicMock

import yaml

from debusine.assets import AssetCategory, KeyPurpose, SigningKeyData
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.debusine import Debusine
from debusine.client.exceptions import DebusineError
from debusine.client.models import AssetResponse, AssetsResponse


class CliCreateAssetTests(BaseCliTests):
    """Tests for the Cli functionality related to creating assets."""

    def patch_asset_create(self) -> MagicMock:
        """Patch Debusine.asset_create."""
        patcher_asset_create = mock.patch.object(
            Debusine, "asset_create", autospec=True
        )
        mocked_asset_create = patcher_asset_create.start()

        self.addCleanup(patcher_asset_create.stop)
        return mocked_asset_create

    def assert_create_signing_asset(
        self,
        use_stdin: bool = True,
        simulate_failure: bool = False,
    ) -> None:
        """Create a debusine:signing-key asset using the CLI."""
        category = AssetCategory.SIGNING_KEY
        asset_id = 2
        workspace = "test"
        data = SigningKeyData(
            purpose=KeyPurpose.OPENPGP_UPLOAD,
            fingerprint="ABC123",
            public_key=base64.b64encode(
                b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n"
                b"-----END PGP PUBLIC KEY BLOCK-----\n"
            ).decode(),
            description="A Description",
        )

        mocked_asset_create = self.patch_asset_create()

        assert_system_exit_code: int | None = None
        if simulate_failure:
            error_detail: dict[str, Any] = {
                "title": "Workspace not found",
                "detail": "Workspace test not found in scope debusine",
            }
            mocked_asset_create.side_effect = DebusineError(**error_detail)
        else:
            mocked_asset_create.return_value = AssetResponse(
                id=asset_id,
                data=data.model_dump(),
                category=category,
                workspace=workspace,
            )

        create_cli_options = [category, "--workspace", workspace]

        yaml_data = yaml.safe_dump(data.model_dump())
        if use_stdin:
            self.enterContext(self.patch_sys_stdin_read(yaml_data))
            create_cli_options.extend(["--data", "-"])
        else:
            data_file_temp = self.create_temporary_file(
                prefix="create-asset",
                contents=yaml_data.encode("UTF-8"),
            )
            create_cli_options.extend(["--data", str(data_file_temp)])

        cli = self.create_cli(["asset", "create", *create_cli_options])

        if simulate_failure:
            exception = self.assertShowsError(cli.execute)
        else:
            stderr, stdout = self.capture_output(
                cli.execute, assert_system_exit_code=assert_system_exit_code
            )

        mocked_asset_create.assert_called_once_with(
            mock.ANY, category=category, data=data, workspace=workspace
        )

        server_url = self.servers[self.default_server]["api-url"]

        if simulate_failure:
            self.assertDebusineError(exception, error_detail)
        else:
            expected_data = {
                "result": "success",
                "message": (
                    f"New asset created in {server_url} in workspace "
                    f"{workspace} with id {asset_id}."
                ),
            }
            expected = yaml.safe_dump(
                expected_data, sort_keys=False, width=math.inf
            )
            if use_stdin:
                expected = f"---\n{expected}"

            self.assertEqual(stdout, expected)
            self.assertEqual(stderr, "")

    def test_create_signing_asset_stdin(self) -> None:
        """Test create an asset with stdin YAML data."""
        self.assert_create_signing_asset(use_stdin=True)

    def test_create_signing_asset_file(self) -> None:
        """Test create an asset with YAML data in a file."""
        self.assert_create_signing_asset(use_stdin=False)

    def test_create_signing_asset_existing(self) -> None:
        """Test create an asset with YAML data in a file."""
        self.assert_create_signing_asset(simulate_failure=True)


class CliListAssetTests(BaseCliTests):
    """Tests for the cli functionality related to listing assets."""

    def patch_asset_list(self) -> MagicMock:
        """Patch Debusine.asset_list."""
        patcher_asset_list = mock.patch.object(
            Debusine, "asset_list", autospec=True
        )
        mocked_asset_list = patcher_asset_list.start()

        self.addCleanup(patcher_asset_list.stop)
        return mocked_asset_list

    def assert_list_signing_assets(
        self,
        args: list[str],
        expected_call: mock._Call,
        simulate_failure: bool = False,
    ) -> None:
        """List assets using the CLI."""
        mocked_asset_list = self.patch_asset_list()
        asset_response = AssetResponse(
            id=2,
            category=AssetCategory.SIGNING_KEY,
            workspace="test",
            data=SigningKeyData(
                purpose=KeyPurpose.OPENPGP_UPLOAD,
                fingerprint="ABC123",
                public_key=base64.b64encode(
                    b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n"
                    b"-----END PGP PUBLIC KEY BLOCK-----\n"
                ).decode(),
                description="A Description",
            ).model_dump(),
            work_request=12,
        )
        cli = self.create_cli(["asset", "list", *args])

        if simulate_failure:
            error_details: dict[str, Any] = {
                "title": "Workspace not found",
                "detail": "Workspace test not found in scope debusine",
            }
            mocked_asset_list.side_effect = DebusineError(**error_details)
            exception = self.assertShowsError(cli.execute)
        else:
            mocked_asset_list.return_value = AssetsResponse([asset_response])
            stderr, stdout = self.capture_output(cli.execute)

        mocked_asset_list.assert_has_calls([expected_call])
        self.assertEqual(len(mocked_asset_list.mock_calls), 1)

        if simulate_failure:
            self.assertDebusineError(exception, error_details)
        else:
            expected_data: list[dict[str, Any]] | dict[str, Any] = [
                asset_response.model_dump()
            ]
            expected = yaml.safe_dump(
                expected_data, sort_keys=False, width=math.inf
            )

            self.assertEqual(stdout, expected)
            self.assertEqual(stderr, "")

    def test_list_assets_by_id(self) -> None:
        """Test list an asset by ID."""
        self.assert_list_signing_assets(
            args=["--id", "2"],
            expected_call=mock.call(
                mock.ANY, asset_id=2, work_request=None, workspace=None
            ),
        )

    def test_list_assets_by_workspace(self) -> None:
        """Test list an asset by workspace."""
        self.assert_list_signing_assets(
            args=["--workspace", "test"],
            expected_call=mock.call(
                mock.ANY, asset_id=None, work_request=None, workspace="test"
            ),
        )

    def test_list_assets_by_work_request(self) -> None:
        """Test list an asset by work_request."""
        self.assert_list_signing_assets(
            args=["--work-request", "12"],
            expected_call=mock.call(
                mock.ANY, asset_id=None, work_request=12, workspace=None
            ),
        )

    def test_list_assets_failure(self) -> None:
        """Test list an asset by work_request."""
        self.assert_list_signing_assets(
            args=["--work-request", "12"],
            expected_call=mock.call(
                mock.ANY, asset_id=None, work_request=12, workspace=None
            ),
            simulate_failure=True,
        )

    def test_list_assets_unfiltered(self) -> None:
        cli = self.create_cli(["asset", "list"])

        stderr, _ = self.capture_output(cli.execute, assert_system_exit_code=3)
        self.assertEqual(
            stderr,
            (
                "At least one of --id, --workspace, and --work-request must "
                "be specified.\n"
            ),
        )
