import json
import ssl
import unittest
import urllib.error
from unittest.mock import patch

from lib.registro_docker import (
    ErroRegistroDocker,
    RegistroDocker,
    separar_referencia_imagem,
)


class TestSepararReferenciaImagem(unittest.TestCase):
    def test_referencia_completa_com_registro_porta_e_tag(self):
        self.assertEqual(
            separar_referencia_imagem(
                "gitlab.microuniverso.com.br:5050/microuniverso/mufenix/centralrelatorios/central-de-relatorios:1.6.0"
            ),
            (
                "gitlab.microuniverso.com.br:5050",
                "microuniverso/mufenix/centralrelatorios/central-de-relatorios",
                "1.6.0"
            )
        )

    def test_referencia_sem_tag(self):
        self.assertEqual(
            separar_referencia_imagem("gitlab.microuniverso.com.br:5050/microuniverso/mufenix/enterprise/enterprise"),
            ("gitlab.microuniverso.com.br:5050", "microuniverso/mufenix/enterprise/enterprise", "")
        )

    def test_registro_sem_porta(self):
        self.assertEqual(
            separar_referencia_imagem("registry.gitlab.com/mufenix/monitorfreterapido/monitor-frete-rapido:1.2.3"),
            ("registry.gitlab.com", "mufenix/monitorfreterapido/monitor-frete-rapido", "1.2.3")
        )

    def test_imagem_publica_sem_registro(self):
        self.assertEqual(separar_referencia_imagem("nginx:1.26.0"), ("", "nginx", "1.26.0"))
        self.assertEqual(separar_referencia_imagem("mongo"), ("", "mongo", ""))

    def test_referencia_vazia(self):
        self.assertEqual(separar_referencia_imagem(""), ("", "", ""))
        self.assertEqual(separar_referencia_imagem(None), ("", "", ""))


class RegistroDockerFake(RegistroDocker):
    """Registro com a camada HTTP substituida por respostas roteirizadas."""

    def __init__(self, respostas, **kwargs):
        super().__init__(**kwargs)
        self.respostas = respostas
        self.urls_requisitadas = []

    def _requisitar(self, url, cabecalhos=None, metodo="GET"):
        self.urls_requisitadas.append(url)

        # Do padrao mais especifico para o mais generico, senao a sonda "/v2/"
        # casaria com qualquer URL da API.
        for padrao in sorted(self.respostas, key=len, reverse=True):
            if padrao in url:
                return self.respostas[padrao]

        raise AssertionError(f"URL sem resposta roteirizada no teste: {url}")


DESAFIO = (
    401,
    {"WWW-Authenticate": 'Bearer realm="https://gitlab.exemplo/jwt/auth",service="container_registry"'},
    b""
)

TOKEN_OK = (200, {}, json.dumps({"token": "token-de-teste"}).encode("utf-8"))


def montar_registro(respostas_extra):
    respostas = {
        "/v2/": DESAFIO,
        "https://gitlab.exemplo/jwt/auth": TOKEN_OK,
    }
    respostas.update(respostas_extra)

    return RegistroDockerFake(
        respostas,
        registro="registro.exemplo:5050",
        usuario="oauth2",
        senha="token"
    )


class TestListarTags(unittest.TestCase):
    def test_lista_tags_do_repositorio(self):
        registro = montar_registro({
            "/tags/list": (200, {}, json.dumps({"tags": ["1.0.0", "1.1.0"]}).encode("utf-8")),
        })

        self.assertEqual(registro.listar_tags("grupo/projeto"), ["1.0.0", "1.1.0"])

    def test_segue_paginacao_pelo_header_link(self):
        primeira = (
            200,
            {"Link": '</v2/grupo/projeto/tags/list?n=100&last=1.1.0>; rel="next"'},
            json.dumps({"tags": ["1.0.0", "1.1.0"]}).encode("utf-8")
        )
        segunda = (200, {}, json.dumps({"tags": ["1.2.0"]}).encode("utf-8"))

        class RegistroPaginado(RegistroDockerFake):
            def __init__(self):
                super().__init__(
                    {"/v2/": DESAFIO, "https://gitlab.exemplo/jwt/auth": TOKEN_OK},
                    registro="registro.exemplo:5050",
                    usuario="oauth2",
                    senha="token"
                )
                self.chamadas_tags = 0

            def _requisitar(self, url, cabecalhos=None, metodo="GET"):
                if "/tags/list" in url:
                    self.chamadas_tags += 1
                    return primeira if self.chamadas_tags == 1 else segunda

                return super()._requisitar(url, cabecalhos, metodo)

        registro = RegistroPaginado()

        self.assertEqual(registro.listar_tags("grupo/projeto"), ["1.0.0", "1.1.0", "1.2.0"])

    def test_resultado_e_cacheado(self):
        registro = montar_registro({
            "/tags/list": (200, {}, json.dumps({"tags": ["1.0.0"]}).encode("utf-8")),
        })

        registro.listar_tags("grupo/projeto")
        quantidade_apos_primeira = len(registro.urls_requisitadas)
        registro.listar_tags("grupo/projeto")

        self.assertEqual(len(registro.urls_requisitadas), quantidade_apos_primeira)

    def test_repositorio_inexistente_gera_erro(self):
        registro = montar_registro({"/tags/list": (404, {}, b"")})

        with self.assertRaises(ErroRegistroDocker) as contexto:
            registro.listar_tags("grupo/inexistente")

        self.assertIn("nao encontrado", str(contexto.exception))

    def test_credencial_recusada_marca_erro_de_autenticacao(self):
        registro = RegistroDockerFake(
            {"/v2/": DESAFIO, "https://gitlab.exemplo/jwt/auth": (401, {}, b"")},
            registro="registro.exemplo:5050",
            usuario="oauth2",
            senha="token-errado"
        )

        with self.assertRaises(ErroRegistroDocker) as contexto:
            registro.listar_tags("grupo/projeto")

        self.assertTrue(contexto.exception.eh_erro_autenticacao)

    def test_tags_nulas_retornam_lista_vazia(self):
        registro = montar_registro({
            "/tags/list": (200, {}, json.dumps({"tags": None}).encode("utf-8")),
        })

        self.assertEqual(registro.listar_tags("grupo/projeto"), [])


class TestTagExiste(unittest.TestCase):
    def test_tag_existente(self):
        registro = montar_registro({"/manifests/": (200, {}, b"{}")})

        self.assertTrue(registro.tag_existe("grupo/projeto", "1.0.0"))

    def test_tag_inexistente(self):
        registro = montar_registro({"/manifests/": (404, {}, b"")})

        self.assertFalse(registro.tag_existe("grupo/projeto", "9.9.9"))

    def test_resultado_e_cacheado(self):
        registro = montar_registro({"/manifests/": (200, {}, b"{}")})

        registro.tag_existe("grupo/projeto", "1.0.0")
        quantidade_apos_primeira = len(registro.urls_requisitadas)
        registro.tag_existe("grupo/projeto", "1.0.0")

        self.assertEqual(len(registro.urls_requisitadas), quantidade_apos_primeira)

    def test_status_inesperado_gera_erro(self):
        registro = montar_registro({"/manifests/": (500, {}, b"")})

        with self.assertRaises(ErroRegistroDocker):
            registro.tag_existe("grupo/projeto", "1.0.0")




class RespostaFake:
    def __init__(self, status=200, corpo=b"{}"):
        self.status = status
        self.headers = {}
        self._corpo = corpo

    def read(self):
        return self._corpo

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False


def erro_de_certificado():
    return urllib.error.URLError(
        ssl.SSLCertVerificationError(
            "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: "
            "unable to get local issuer certificate (_ssl.c:1032)"
        )
    )


class TestFalhaDeCertificado(unittest.TestCase):
    """
    Servidor de cliente sem a raiz certificadora no armazenamento do Windows falhava a implantacao
    inteira com "unable to get local issuer certificate". A consulta passa a repetir com as raizes
    do certifi, que ja vem instalado como dependencia do requests.
    """

    def _montar_registro(self):
        return RegistroDocker(registro="registro.exemplo:5050", usuario="oauth2", senha="token")

    @patch("lib.registro_docker.echo")
    @patch("lib.registro_docker.ssl.create_default_context")
    @patch("lib.registro_docker._bundle_certificados_certifi", return_value="C:/certifi/cacert.pem")
    @patch("urllib.request.urlopen")
    def test_repete_com_bundle_do_certifi_e_conclui(self, urlopen, _bundle, _contexto, _echo):
        urlopen.side_effect = [erro_de_certificado(), RespostaFake(200, b'{"ok":true}')]

        registro = self._montar_registro()
        status, _, corpo = registro._requisitar("https://registro.exemplo:5050/v2/")

        self.assertEqual(status, 200)
        self.assertEqual(corpo, b'{"ok":true}')
        self.assertTrue(registro._usando_bundle_certifi)
        self.assertEqual(urlopen.call_count, 2)

    @patch("lib.registro_docker.echo")
    @patch("lib.registro_docker.ssl.create_default_context")
    @patch("lib.registro_docker._bundle_certificados_certifi", return_value="C:/certifi/cacert.pem")
    @patch("urllib.request.urlopen")
    def test_repete_uma_unica_vez_quando_o_certifi_tambem_falha(self, urlopen, _bundle, _contexto, _echo):
        urlopen.side_effect = erro_de_certificado()

        with self.assertRaises(ErroRegistroDocker) as contexto:
            self._montar_registro()._requisitar("https://registro.exemplo:5050/v2/")

        self.assertEqual(urlopen.call_count, 2)
        self.assertIn("certificado", str(contexto.exception))

    @patch("lib.registro_docker._bundle_certificados_certifi", return_value=None)
    @patch("urllib.request.urlopen")
    def test_sem_certifi_orienta_como_resolver(self, urlopen, _bundle):
        urlopen.side_effect = erro_de_certificado()

        with self.assertRaises(ErroRegistroDocker) as contexto:
            self._montar_registro()._requisitar("https://registro.exemplo:5050/v2/")

        mensagem = str(contexto.exception)

        self.assertEqual(urlopen.call_count, 1)
        self.assertIn("SSL_CERT_FILE", mensagem)
        self.assertIn("versao especifica", mensagem)

    @patch("urllib.request.urlopen")
    def test_erro_de_rede_comum_nao_dispara_a_repeticao(self, urlopen):
        urlopen.side_effect = urllib.error.URLError("timed out")

        with self.assertRaises(ErroRegistroDocker):
            self._montar_registro()._requisitar("https://registro.exemplo:5050/v2/")

        self.assertEqual(urlopen.call_count, 1)


if __name__ == "__main__":
    unittest.main()
