from typing import Optional, Tuple
from unittest.mock import Mock

import pytest
from requests.exceptions import ConnectionError

from ggshield.__main__ import cli
from ggshield.core.config import Config
from ggshield.core.constants import DEFAULT_INSTANCE_URL
from ggshield.core.errors import ExitCode

from ..utils import add_instance_config


class TestAuthLogout:
    def test_logout_no_account_config(self, cli_fs_runner):
        """
        GIVEN -
        WHEN using the logout command and no token is saved in the configuration
        THEN the command exits with an explanatory message
        """
        instance_url = "https://dashboard.gitguardian.com"
        add_instance_config(with_account=False)
        exit_code, output = self.run_cmd(cli_fs_runner, instance_url)

        assert exit_code == ExitCode.UNEXPECTED_ERROR, output
        assert output == (
            f"Error: No token found for instance {instance_url}\n"
            "First try to login by running:\n"
            "  ggshield auth login\n"
        )

    @pytest.mark.parametrize(
        "instance_url", (DEFAULT_INSTANCE_URL, "https://some-gg-instance.com")
    )
    @pytest.mark.parametrize("revoke", (True, False))
    def test_valid_logout(self, revoke, instance_url, monkeypatch, cli_fs_runner):
        """
        GIVEN a saved instance configuration
        WHEN running the logout command
        THEN the specified instance data is erased
        AND the request for revocation is made if no flag was included
        """
        unrelated_url = "https://some-unrelated-gg-instance.com"

        post_mock = Mock(return_value=Mock(status_code=204, ok=True))
        monkeypatch.setattr("ggshield.core.client.GGClient.post", post_mock)

        token_name = "My great token"
        add_instance_config(instance_url=instance_url, token_name=token_name)

        # unrelated config that should remain unchanged
        add_instance_config(instance_url=unrelated_url)

        exit_code, output = self.run_cmd(cli_fs_runner, instance_url, revoke=revoke)

        if revoke:
            post_mock.assert_called_once()
        else:
            post_mock.assert_not_called()

        config = Config()
        assert config.auth_config.get_instance(instance_url).account is None
        assert (
            config.auth_config.get_instance(unrelated_url).account is not None
        ), "the unrelated instance should not be affected."

        assert exit_code == ExitCode.SUCCESS, output
        instance_url = instance_url or "https://dashboard.gitguardian.com"

        expected_output = f"Successfully logged out for instance {instance_url}\n\n"

        if revoke:
            expected_output += (
                "Your personal access token has been revoked and removed "
                "from your configuration.\n"
            )
        else:
            expected_output += (
                "Your personal access token has been removed "
                "from your configuration.\n"
            )

        assert output == expected_output

    def test_logout_revoke_timeout(self, monkeypatch, cli_fs_runner):
        """
        GIVEN a saved instance configuration
        WHEN running the logout command (with implied token revokation)
        AND the revoke request gets a timeout
        THEN the config remains unchanged
        AND the command exits with an explanatory message
        """

        post_mock = Mock(side_effect=ConnectionError("Http max retry"))
        monkeypatch.setattr("ggshield.core.client.GGClient.post", post_mock)

        add_instance_config()
        exit_code, output = self.run_cmd(cli_fs_runner)

        post_mock.assert_called_once()
        config = Config()
        assert config.auth_config.get_instance(DEFAULT_INSTANCE_URL).account is not None

        assert exit_code == ExitCode.UNEXPECTED_ERROR, output
        assert output == (
            "Error: Could not connect to GitGuardian.\n"
            "Please check your internet connection and if the specified URL is correct.\n"
        )

    def test_logout_server_error(self, monkeypatch, cli_fs_runner):
        """
        GIVEN a saved instance configuration
        WHEN running the logout command (with implied token revokation)
        AND the revoke request gets a server error response
        THEN the config remains unchanged
        AND the command exits with an explanatory message
        """
        post_mock = Mock(return_value=Mock(status_code=500, ok=False))
        monkeypatch.setattr("ggshield.core.client.GGClient.post", post_mock)

        add_instance_config()
        exit_code, output = self.run_cmd(cli_fs_runner)

        post_mock.assert_called_once()
        config = Config()
        assert config.auth_config.get_instance(DEFAULT_INSTANCE_URL).account is not None

        assert exit_code == ExitCode.AUTHENTICATION_ERROR, output
        assert output == (
            "Error: Could not perform the logout command "
            "because your token is already revoked or invalid.\n"
            "Please try with the following command:\n"
            "  ggshield auth logout --no-revoke\n"
        )

    def test_logout_all(self, monkeypatch, cli_fs_runner):
        """
        GIVEN several saved instances
        WHEN running the logout command with --all parameter
        THEN all tokens are revoked
        AND all account configs are deleted
        """
        post_mock = Mock(return_value=Mock(status_code=204, ok=True))
        monkeypatch.setattr("ggshield.core.client.GGClient.post", post_mock)

        for instance_url in [
            DEFAULT_INSTANCE_URL,
            "https://some-gg-instance.com",
            "https://some-other-gg-instance.com",
        ]:
            add_instance_config(instance_url)

        exit_code, output = self.run_cmd(cli_fs_runner, all_tokens=True)
        assert len(post_mock.call_args_list) == 3

        for instance in Config().auth_config.instances:
            assert instance.account is None, output

        assert exit_code == ExitCode.SUCCESS, output

    @staticmethod
    def run_cmd(
        cli_fs_runner,
        instance: Optional[str] = None,
        revoke: bool = True,
        all_tokens: bool = False,
    ) -> Tuple[int, str]:
        cmd = ["auth", "logout"]
        if instance is not None:
            cmd.append("--instance=" + instance)
        if not revoke:
            cmd.append("--no-revoke")
        if all_tokens:
            cmd.append("--all")
        result = cli_fs_runner.invoke(cli, cmd, color=False)
        return result.exit_code, result.output
