import json.encoder
import os
import re

import pytest

from .md_conf import MDConf
from .md_env import MDTestEnv


@pytest.mark.skipif(condition=not MDTestEnv.has_acme_eab(),
                    reason="ACME test server does not support External Account Binding")
class TestEab:

    @pytest.fixture(autouse=True, scope='class')
    def _class_scope(self, env, acme):
        acme.start(config='eab')
        env.check_acme()
        env.clear_store()
        MDConf(env).install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'

    @pytest.fixture(autouse=True, scope='function')
    def _method_scope(self, env, request):
        env.clear_store()
        self.test_domain = env.get_request_domain(request)

    def test_md_750_001(self, env):
        # md without EAB configured
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
            ]
        )

    def test_md_750_002(self, env):
        # md with known EAB KID and non base64 hmac key configured
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding kid-1 äöüß")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'apache:eab-hmac-invalid'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # external account binding HMAC value is not valid base64
            ],
            matches = [
                r'.*problem\[apache:eab-hmac-invalid\].*'
            ]
        )

    def test_md_750_003(self, env):
        # md with empty EAB KID configured
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding \" \" bm90IGEgdmFsaWQgaG1hYwo=")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] in [
            'urn:ietf:params:acme:error:unauthorized',
            'urn:ietf:params:acme:error:malformed',
        ]
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # the field 'kid' references a key that is not known to the ACME server
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:(unauthorized|malformed).*'
            ]
        )

    def test_md_750_004(self, env):
        # md with unknown EAB KID configured
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding key-x bm90IGEgdmFsaWQgaG1hYwo=")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] in [
            'urn:ietf:params:acme:error:unauthorized',
            'urn:ietf:params:acme:error:malformed',
        ]
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # the field 'kid' references a key that is not known to the ACME server
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:(unauthorized|malformed).*'
            ]
        )

    def test_md_750_005(self, env):
        # md with known EAB KID but wrong HMAC configured
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding kid-1 bm90IGEgdmFsaWQgaG1hYwo=")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] in [
            'urn:ietf:params:acme:error:unauthorized',
            'urn:ietf:params:acme:error:malformed',
        ]
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # external account binding JWS verification error: square/go-jose: error in cryptographic primitive
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:(unauthorized|malformed).*'
            ]
        )

    def test_md_750_010(self, env):
        # md with correct EAB configured
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        # this is one of the values in conf/pebble-eab.json
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion(domains)

    def test_md_750_011(self, env):
        # first one md with EAB, then one without, works only for the first
        # as the second is unable to reuse the account
        domain_a = f"a{self.test_domain}"
        domain_b = f"b{self.test_domain}"
        conf = MDConf(env)
        conf.start_md([domain_a])
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.end_md()
        conf.add_vhost(domains=[domain_a])
        conf.add_md([domain_b])
        conf.add_vhost(domains=[domain_b])
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion([domain_a], restart=False)
        md = env.await_error(domain_b)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
            ]
        )

    def test_md_750_012(self, env):
        # first one md without EAB, then one with
        # first one fails, second works
        domain_a = f"a{self.test_domain}"
        domain_b = f"b{self.test_domain}"
        conf = MDConf(env)
        conf.add_md([domain_a])
        conf.add_vhost(domains=[domain_a])
        conf.start_md([domain_b])
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.end_md()
        conf.add_vhost(domains=[domain_b])
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion([domain_b], restart=False)
        md = env.await_error(domain_a)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
            ]
        )

    def test_md_750_013(self, env):
        # 2 mds with the same EAB, should one create a single account
        domain_a = f"a{self.test_domain}"
        domain_b = f"b{self.test_domain}"
        conf = MDConf(env)
        conf.start_md([domain_a])
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.end_md()
        conf.add_vhost(domains=[domain_a])
        conf.start_md([domain_b])
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.end_md()
        conf.add_vhost(domains=[domain_b])
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion([domain_a, domain_b])
        md_a = env.get_md_status(domain_a)
        md_b = env.get_md_status(domain_b)
        assert md_a['ca'] == md_b['ca']

    def test_md_750_014(self, env):
        # md with correct EAB, get cert, change to another correct EAB
        # needs to create a new account
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion(domains)
        md_1 = env.get_md_status(domain)
        conf = MDConf(env)
        # this is another one of the values in conf/pebble-eab.json
        # add a dns name to force renewal
        domains = [domain, f'www.{domain}']
        conf.add("MDExternalAccountBinding kid-2 b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion(domains)
        md_2 = env.get_md_status(domain)
        assert md_1['ca'] != md_2['ca']

    def test_md_750_015(self, env):
        # md with correct EAB, get cert, change to no EAB
        # needs to fail
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion(domains)
        conf = MDConf(env)
        # this is another one of the values in conf/pebble-eab.json
        # add a dns name to force renewal
        domains = [domain, f'www.{domain}']
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_error(domain)
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
            ]
        )

    def test_md_750_016(self, env):
        # md with correct EAB, get cert, change to invalid EAB
        # needs to fail
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion(domains)
        conf = MDConf(env)
        # this is another one of the values in conf/pebble-eab.json
        # add a dns name to force renewal
        domains = [domain, f'www.{domain}']
        conf.add("MDExternalAccountBinding kid-invalud blablabalbalbla")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_error(domain)
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # the field 'kid' references a key that is not known to the ACME server
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:unauthorized.*'
            ]
        )

    def test_md_750_017(self, env):
        # md without EAB explicitly set to none
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding kid-1 zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W")
        conf.start_md(domains)
        conf.add("MDExternalAccountBinding none")
        conf.end_md()
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        md = env.await_error(domain)
        assert md['renewal']['errors'] > 0
        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
        #
        env.httpd_error_log.ignore_recent(
            lognos = [
                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
            ],
            matches = [
                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
            ]
        )

    def test_md_750_018(self, env):
        # md with EAB file that does not exist
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding does-not-exist")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_fail() == 0
        assert re.search(r'.*file not found:', env.apachectl_stderr), env.apachectl_stderr

    def test_md_750_019(self, env):
        # md with EAB file that is not valid JSON
        domain = self.test_domain
        domains = [domain]
        eab_file = os.path.join(env.server_dir, 'eab.json')
        with open(eab_file, 'w') as fd:
            fd.write("something not JSON\n")
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding eab.json")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_fail() == 0
        assert re.search(r'.*error reading JSON file.*', env.apachectl_stderr), env.apachectl_stderr

    def test_md_750_020(self, env):
        # md with EAB file that is JSON, but missind kid
        domain = self.test_domain
        domains = [domain]
        eab_file = os.path.join(env.server_dir, 'eab.json')
        with open(eab_file, 'w') as fd:
            eab = {'something': 1, 'other': 2}
            fd.write(json.encoder.JSONEncoder().encode(eab))
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding eab.json")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_fail() == 0
        assert re.search(r'.*JSON does not contain \'kid\' element.*', env.apachectl_stderr), env.apachectl_stderr

    def test_md_750_021(self, env):
        # md with EAB file that is JSON, but missind hmac
        domain = self.test_domain
        domains = [domain]
        eab_file = os.path.join(env.server_dir, 'eab.json')
        with open(eab_file, 'w') as fd:
            eab = {'kid': 'kid-1', 'other': 2}
            fd.write(json.encoder.JSONEncoder().encode(eab))
        conf = MDConf(env)
        conf.add("MDExternalAccountBinding eab.json")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_fail() == 0
        assert re.search(r'.*JSON does not contain \'hmac\' element.*', env.apachectl_stderr), env.apachectl_stderr

    def test_md_750_022(self, env):
        # md with EAB file that has correct values
        domain = self.test_domain
        domains = [domain]
        eab_file = os.path.join(env.server_dir, 'eab.json')
        with open(eab_file, 'w') as fd:
            eab = {'kid': 'kid-1', 'hmac': 'zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W'}
            fd.write(json.encoder.JSONEncoder().encode(eab))
        domain = self.test_domain
        domains = [domain]
        conf = MDConf(env)
        # this is one of the values in conf/pebble-eab.json
        conf.add("MDExternalAccountBinding eab.json")
        conf.add_md(domains)
        conf.add_vhost(domains=domains)
        conf.install()
        assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
        assert env.await_completion(domains)
