"""
:codeauthor: Herbert Buurman <herbert.buurman@ogd.nl>
"""


import datetime
import os
import textwrap

# Import Salt Module
import salt.modules.acme as acme
import salt.utils.dictupdate
import salt.utils.platform
from salt.exceptions import SaltInvocationError
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.mock import MagicMock, patch
from tests.support.unit import TestCase


class AcmeTestCase(TestCase, LoaderModuleMockMixin):
    """
    Test cases for salt.modules.acme
    """

    def setup_loader_modules(self):
        return {acme: {}}

    def test_certs(self):
        """
        Test listing certs
        """
        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "file.readdir": MagicMock(
                    return_value=[".", "..", "README", "test_expired", "test_valid"]
                )
            },
        ), patch(
            "os.path.isdir",
            side_effect=lambda path: path
            in [
                os.path.join(acme.LE_LIVE, "test_expired"),
                os.path.join(acme.LE_LIVE, "test_valid"),
            ],
        ):
            self.assertEqual(acme.certs(), ["test_expired", "test_valid"])

    def test_has(self):
        """
        Test checking if certificate (does not) exist.
        """
        with patch.dict(
            acme.__salt__, {"file.file_exists": MagicMock(return_value=True)}
        ):  # pylint: disable=no-member
            self.assertTrue(acme.has("test_expired"))
        with patch.dict(
            acme.__salt__, {"file.file_exists": MagicMock(return_value=False)}
        ):  # pylint: disable=no-member
            self.assertFalse(acme.has("test_invalid"))

    def test_needs_renewal(self):
        """
        Test if expired certs do indeed need renewal.
        """
        expired = (
            datetime.date.today()
            - datetime.timedelta(days=3)
            - datetime.date(1970, 1, 1)
        )
        valid = (
            datetime.date.today()
            + datetime.timedelta(days=3)
            - datetime.date(1970, 1, 1)
        )
        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "tls.cert_info": MagicMock(
                    return_value={"not_after": expired.total_seconds()}
                )
            },
        ):
            self.assertTrue(acme.needs_renewal("test_expired"))
        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "tls.cert_info": MagicMock(
                    return_value={"not_after": valid.total_seconds()}
                )
            },
        ):
            self.assertFalse(acme.needs_renewal("test_valid"))
            # Test with integer window parameter
            self.assertTrue(acme.needs_renewal("test_valid", window=5))
            # Test with string-like window parameter
            self.assertTrue(acme.needs_renewal("test_valid", window="5"))
            # Test with 'force' parameter
            self.assertTrue(acme.needs_renewal("test_valid", window="force"))
            # Test with 'true' parameter
            self.assertTrue(acme.needs_renewal("test_valid", window=True))
            # Test with invalid window parameter
            self.assertRaises(
                SaltInvocationError, acme.needs_renewal, "test_valid", window="foo"
            )

    def test_expires(self):
        """
        Test if expires function functions properly.
        """
        test_value = datetime.datetime.today() - datetime.timedelta(days=3)
        test_stamp = test_value - datetime.datetime(1970, 1, 1)
        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "tls.cert_info": MagicMock(
                    return_value={"not_after": test_stamp.total_seconds()}
                )
            },
        ):
            self.assertEqual(
                acme.expires("test_expired"),
                datetime.datetime.fromtimestamp(test_stamp.total_seconds()).isoformat(),
            )

    def test_info(self):
        """
        Test certificate information retrieval.
        """
        certinfo_tls_result = {
            "not_after": 1559471377,
            "signature_algorithm": "sha256WithRSAEncryption",
            "extensions": {},
            "fingerprint": (
                "FB:A4:5F:71:D6:5D:6C:B6:1D:2C:FD:91:09:2C:1C:52:"
                "3C:EC:B6:4D:1A:95:65:37:04:D0:E2:5E:C7:64:0C:9C"
            ),
            "serial_number": 6461481982668892235,
            "issuer": {},
            "not_before": 1559557777,
            "subject": {},
        }
        certinfo_x509_result = {
            "Not After": "2019-06-02 10:29:37",
            "Subject Hash": "54:3B:6C:A4",
            "Serial Number": "59:AB:CB:A0:FB:90:E8:4B",
            "SHA1 Finger Print": (
                "F1:8D:F3:26:1B:D3:88:32:CD:B6:FA:3B:85:58:DA:C7:6F:62:BE:7E"
            ),
            "SHA-256 Finger Print": (
                "FB:A4:5F:71:D6:5D:6C:B6:1D:2C:FD:91:09:2C:1C:52:"
                "3C:EC:B6:4D:1A:95:65:37:04:D0:E2:5E:C7:64:0C:9C"
            ),
            "MD5 Finger Print": "95:B5:96:9B:42:A5:9E:20:78:FD:99:09:4B:21:1E:97",
            "Version": 3,
            "Key Size": 2048,
            "Public Key": (
                "-----BEGIN PUBLIC KEY-----\n"
                "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsVO2vwQPKU92PSBnuGid\n"
                "k8t6KWVE2jEBM10u7CgqQmD/JCnYflEHAo1nOsD7wxdhBrxhf5Qs+pEX1HOsh8VA\n"
                "HDTim0iE8nQVJ0Iuen2SrwaWMhwKmZTSJRYMgd46oCMi2RdlCvcgF2Hw6RTwF7FT\n"
                "hnksc4HBT91XddnP32N558tOT3YejafQNvClz5WcR+E0JzqGrV/+wfe3o+j/q5eK\n"
                "UowttWazeSMvuROtqj/fEk0rop4D14pgzZqWi30tjwhJNl6fSPFWBrLEHGNyDJ+O\n"
                "zfov0B2MRLJibH7GMkOCwsP2g1lVOReqcml+ju6zAKW8nHBTRg0iXB18Ifxef57Y\n"
                "AQIDAQAB\n"
                "-----END PUBLIC KEY-----\n"
            ),
            "Issuer": {},
            "Issuer Hash": "54:3B:6C:A4",
            "Not Before": "2019-06-03 10:29:37",
            "Subject": {},
        }

        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "file.file_exists": MagicMock(return_value=True),
                "tls.cert_info": MagicMock(return_value=certinfo_tls_result),
            },
        ):
            self.assertEqual(acme.info("test"), certinfo_tls_result)
        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "file.file_exists": MagicMock(return_value=True),
                "x509.read_certificate": MagicMock(return_value=certinfo_x509_result),
            },
        ):
            self.assertEqual(acme.info("test"), certinfo_x509_result)
        with patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "file.file_exists": MagicMock(return_value=True),
                "cmd.run": MagicMock(return_value="foo"),
            },
        ):
            self.assertEqual(acme.info("test"), {"text": "foo"})

    def test_cert(self):
        """
        Test certificate retrieval/renewal
        """
        valid_timestamp = (
            datetime.datetime.now()
            + datetime.timedelta(days=30)
            - datetime.datetime(1970, 1, 1, 0, 0, 0, 0)
        ).total_seconds()
        expired_timestamp = (
            datetime.datetime.now()
            - datetime.timedelta(days=3)
            - datetime.datetime(1970, 1, 1, 0, 0, 0, 0)
        ).total_seconds()
        cmd_new_cert = {
            "stdout": textwrap.dedent(
                """
                IMPORTANT NOTES:
                 - Congratulations! Your certificate and chain have been saved at:
                   /etc/letsencrypt/live/test/fullchain.pem
                   Your key file has been saved at:
                   /etc/letsencrypt/live/test/privkey.pem
                   Your cert will expire on 2019-08-07. To obtain a new or tweaked
                   version of this certificate in the future, simply run certbot
                   again. To non-interactively renew *all* of your certificates, run
                   "certbot renew"
                 - If you like Certbot, please consider supporting our work by:

                   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
                   Donating to EFF:                    https://eff.org/donate-le
                """
            ),
            "stderr": textwrap.dedent(
                """
                Saving debug log to /var/log/letsencrypt/letsencrypt.log
                Plugins selected: Authenticator standalone, Installer None
                Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org
                Obtaining a new certificate
                Resetting dropped connection: acme-v02.api.letsencrypt.org
                """
            ),
            "retcode": 0,
        }
        result_new_cert = {
            "comment": "Certificate test obtained",
            "not_after": datetime.datetime.fromtimestamp(valid_timestamp).isoformat(),
            "changes": {"mode": "0640"},
            "result": True,
        }

        cmd_no_renew = {
            "stdout": textwrap.dedent(
                """
                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                Certificate not yet due for renewal; no action taken.
                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                """
            ),
            "stderr": textwrap.dedent(
                """Saving debug log to /var/log/letsencrypt/letsencrypt.log
                Plugins selected: Authenticator standalone, Installer None
                Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org
                Cert not yet due for renewal
                Keeping the existing certificate
                """
            ),
            "retcode": 0,
        }
        if salt.utils.platform.is_freebsd():
            result_no_renew = {
                "comment": "Certificate "
                + os.path.join("/usr/local/etc/letsencrypt/live/test", "cert.pem")
                + " unchanged",
                "not_after": datetime.datetime.fromtimestamp(
                    valid_timestamp
                ).isoformat(),
                "changes": {},
                "result": True,
            }
        else:
            result_no_renew = {
                "comment": "Certificate "
                + os.path.join("/etc/letsencrypt/live/test", "cert.pem")
                + " unchanged",
                "not_after": datetime.datetime.fromtimestamp(
                    valid_timestamp
                ).isoformat(),
                "changes": {},
                "result": True,
            }
        result_renew = {
            "comment": "Certificate test renewed",
            "not_after": datetime.datetime.fromtimestamp(expired_timestamp).isoformat(),
            "changes": {},
            "result": True,
        }

        # Test fetching new certificate
        with patch("salt.modules.acme.LEA", "certbot"), patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "cmd.run_all": MagicMock(return_value=cmd_new_cert),
                "file.file_exists": MagicMock(return_value=False),
                "tls.cert_info": MagicMock(return_value={"not_after": valid_timestamp}),
                "file.check_perms": MagicMock(
                    side_effect=lambda a, x, b, c, d, follow_symlinks: (
                        salt.utils.dictupdate.set_dict_key_value(
                            x, "changes:mode", "0640"
                        ),
                        None,
                    )
                ),
            },
        ):
            self.assertEqual(acme.cert("test"), result_new_cert)
            self.assertEqual(
                acme.cert("testing.example.com", certname="test"), result_new_cert
            )
        # Test not renewing a valid certificate
        with patch("salt.modules.acme.LEA", "certbot"), patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "cmd.run_all": MagicMock(return_value=cmd_no_renew),
                "file.file_exists": MagicMock(return_value=True),
                "tls.cert_info": MagicMock(return_value={"not_after": valid_timestamp}),
                "file.check_perms": MagicMock(
                    side_effect=lambda a, x, b, c, d, follow_symlinks: (
                        salt.utils.dictupdate.set_dict_key_value(x, "result", True),
                        None,
                    )
                ),
            },
        ):
            self.assertEqual(acme.cert("test"), result_no_renew)
            self.assertEqual(
                acme.cert("testing.example.com", certname="test"), result_no_renew
            )
        # Test renewing an expired certificate
        with patch("salt.modules.acme.LEA", "certbot"), patch.dict(
            acme.__salt__,
            {  # pylint: disable=no-member
                "cmd.run_all": MagicMock(return_value=cmd_new_cert),
                "file.file_exists": MagicMock(return_value=True),
                "tls.cert_info": MagicMock(
                    return_value={"not_after": expired_timestamp}
                ),
                "file.check_perms": MagicMock(
                    side_effect=lambda a, x, b, c, d, follow_symlinks: (
                        salt.utils.dictupdate.set_dict_key_value(x, "result", True),
                        None,
                    )
                ),
            },
        ):
            self.assertEqual(acme.cert("test"), result_renew)
            self.assertEqual(
                acme.cert("testing.example.com", certname="test"), result_renew
            )
