# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0.  If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.

from datetime import timedelta

import os

import dns.update

from isctest.kasp import Iret
from isctest.run import EnvCmd
from rollover.common import ROLLOVER_MARK
from rollover.setup import fake_lifetime, render_and_sign_zone

import isctest

pytestmark = ROLLOVER_MARK


def bootstrap():
    templates = isctest.template.TemplateEngine(".")

    # Multi-signer zones.
    keygen = EnvCmd("KEYGEN", "-a ECDSA256 -L 3600")
    settime = EnvCmd("SETTIME", "-s")

    # Model 2.
    zonename = "multisigner-model2.kasp"
    isctest.log.info(f"setup {zonename}")
    # Key generation.
    ksk_name = keygen(f"-M 32768:65535 -f KSK {zonename}", cwd="ns3").out.strip()
    zsk_name = keygen(f"-M 32768:65535 {zonename}", cwd="ns3").out.strip()
    # Signing.
    dnskeys = []
    for key_name in [ksk_name, zsk_name]:
        key = isctest.kasp.Key(key_name, keydir="ns3")
        dnskeys.append(key.dnskey)
    # Import a ZSK of another provider into the DNSKEY RRset.
    zsk_extra = keygen(f"-M 0:32767 {zonename}").out.strip()
    key = isctest.kasp.Key(zsk_extra)
    dnskeys.append(key.dnskey)
    # Render zone file.
    outfile = f"{zonename}.db"
    templates = isctest.template.TemplateEngine(".")
    template = "template.db.j2.manual"
    tdata = {
        "fqdn": f"{zonename}.",
        "dnskeys": dnskeys,
        "privaterrs": [],
    }
    templates.render(f"ns3/{outfile}", tdata, template=f"ns3/{template}")

    # We are changing an existing single-signed zone to multi-signed
    # zone where the key tags do not match the dnssec-policy key tag range
    zonename = "single-to-multisigner.kasp"
    isctest.log.info(f"setup {zonename}")
    # Timing metadata.
    TpubN = "now-7d"
    TsbmN = "now-8635mi"  # T - 1d5m
    keytimes = f"-P {TpubN} -A {TpubN}"
    cdstimes = f"-P sync {TsbmN}"
    # Key generation.
    ksk_name = keygen(
        f"-M 0:32767 -f KSK {keytimes} {cdstimes} {zonename}", cwd="ns3"
    ).out.strip()
    zsk_name = keygen(f"-M 0:32767 {keytimes} {zonename}", cwd="ns3").out.strip()
    settime(
        f"-g OMNIPRESENT -d OMNIPRESENT {TpubN} -k OMNIPRESENT {TpubN} -r OMNIPRESENT {TpubN} {ksk_name}",
        cwd="ns3",
    )
    settime(
        f"-g OMNIPRESENT -k OMNIPRESENT {TpubN} -z OMNIPRESENT {TpubN} {zsk_name}",
        cwd="ns3",
    )
    # Signing.
    fake_lifetime(ksk_name, 0)
    fake_lifetime(zsk_name, 0)
    render_and_sign_zone(zonename, [ksk_name, zsk_name])

    return {}


def test_rollover_multisigner(ns3, default_algorithm):
    policy = "multisigner-model2"
    config = {
        "dnskey-ttl": timedelta(hours=1),
        "ds-ttl": timedelta(days=1),
        "max-zone-ttl": timedelta(days=1),
        "parent-propagation-delay": timedelta(hours=1),
        "publish-safety": timedelta(hours=1),
        "retire-safety": timedelta(hours=1),
        "signatures-refresh": timedelta(days=5),
        "signatures-validity": timedelta(days=14),
        "zone-propagation-delay": timedelta(minutes=5),
    }
    ttl = int(config["dnskey-ttl"].total_seconds())

    offset = -timedelta(days=7)
    offval = int(offset.total_seconds())

    def keygen(zone):
        keygen_command = [
            os.environ.get("KEYGEN"),
            "-a",
            default_algorithm.name,
            "-L",
            "3600",
            "-M",
            "0:32767",
            zone,
        ]

        return isctest.run.cmd(keygen_command).out

    zone = "multisigner-model2.kasp"

    isctest.kasp.wait_keymgr_done(ns3, zone)

    isctest.kasp.check_dnssec_verify(ns3, zone)

    key_properties = [
        f"ksk unlimited {default_algorithm.number} {default_algorithm.bits} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden tag-range:32768-65535",
        f"zsk unlimited {default_algorithm.number} {default_algorithm.bits} goal:omnipresent dnskey:rumoured zrrsig:rumoured tag-range:32768-65535",
    ]
    expected = isctest.kasp.policy_to_properties(ttl, key_properties)

    newprops = [
        f"zsk unlimited {default_algorithm.number} {default_algorithm.bits} tag-range:0-32767"
    ]
    expected2 = isctest.kasp.policy_to_properties(ttl, newprops)
    expected2[0].private = False
    expected2[0].legacy = True
    expected = expected + expected2

    ownkeys = isctest.kasp.keydir_to_keylist(zone, ns3.identifier)
    extkeys = isctest.kasp.keydir_to_keylist(zone)
    keys = ownkeys + extkeys
    ksks = [k for k in ownkeys if k.is_ksk()]
    zsks = [k for k in ownkeys if not k.is_ksk()]
    zsks = zsks + extkeys

    isctest.kasp.check_keys(zone, keys, expected)
    for kp in expected:
        kp.set_expected_keytimes(config)
    isctest.kasp.check_keytimes(keys, expected)
    isctest.kasp.check_dnssecstatus(ns3, zone, keys, policy=policy)
    isctest.kasp.check_apex(ns3, zone, ksks, zsks)
    isctest.kasp.check_subdomain(ns3, zone, ksks, zsks)

    # Update zone with ZSK from another provider for zone.
    out = keygen(zone)
    newkeys = isctest.kasp.keystr_to_keylist(out)
    newprops = [
        f"zsk unlimited {default_algorithm.number} {default_algorithm.bits} tag-range:0-32767"
    ]
    expected2 = isctest.kasp.policy_to_properties(ttl, newprops)
    expected2[0].private = False
    expected2[0].legacy = True
    expected = expected + expected2

    dnskey = newkeys[0].dnskey

    update_msg = dns.update.UpdateMessage(zone)
    update_msg.add(dnskey.name, dnskey.ttl, dnskey[0])
    ns3.nsupdate(update_msg)

    isctest.kasp.check_dnssec_verify(ns3, zone)

    keys = keys + newkeys
    zsks = zsks + newkeys
    isctest.kasp.check_keys(zone, keys, expected)
    isctest.kasp.check_apex(ns3, zone, ksks, zsks)
    isctest.kasp.check_subdomain(ns3, zone, ksks, zsks)

    # Remove ZSKs from the other providers for zone.
    dnskey2 = extkeys[0].dnskey
    update_msg = dns.update.UpdateMessage(zone)
    update_msg.delete(dnskey.name, dnskey[0])
    update_msg.delete(dnskey2.name, dnskey2[0])
    ns3.nsupdate(update_msg)

    isctest.kasp.check_dnssec_verify(ns3, zone)

    expected = isctest.kasp.policy_to_properties(ttl, key_properties)
    keys = ownkeys
    ksks = [k for k in ownkeys if k.is_ksk()]
    zsks = [k for k in ownkeys if not k.is_ksk()]
    isctest.kasp.check_keys(zone, keys, expected)
    isctest.kasp.check_apex(ns3, zone, ksks, zsks)
    isctest.kasp.check_subdomain(ns3, zone, ksks, zsks)

    # A zone transitioning from single-signed to multi-signed. We should have
    # the old omnipresent keys outside of the desired key range and the new
    # keys in the desired key range.
    zone = "single-to-multisigner.kasp"

    isctest.kasp.wait_keymgr_done(ns3, zone)

    isctest.kasp.check_dnssec_verify(ns3, zone)

    key_properties = [
        f"ksk unlimited {default_algorithm.number} {default_algorithm.bits} goal:omnipresent dnskey:rumoured krrsig:rumoured ds:hidden tag-range:32768-65535",
        f"zsk unlimited {default_algorithm.number} {default_algorithm.bits} goal:omnipresent dnskey:rumoured zrrsig:hidden tag-range:32768-65535",
        f"ksk unlimited {default_algorithm.number} {default_algorithm.bits} goal:hidden dnskey:omnipresent krrsig:omnipresent ds:omnipresent tag-range:0-32767 offset:{offval}",
        f"zsk unlimited {default_algorithm.number} {default_algorithm.bits} goal:hidden dnskey:omnipresent zrrsig:omnipresent tag-range:0-32767 offset:{offval}",
    ]
    expected = isctest.kasp.policy_to_properties(ttl, key_properties)
    keys = isctest.kasp.keydir_to_keylist(zone, ns3.identifier)
    ksks = [k for k in keys if k.is_ksk()]
    zsks = [k for k in keys if not k.is_ksk()]

    isctest.kasp.check_keys(zone, keys, expected)

    for kp in expected:
        kp.set_expected_keytimes(config)

    start = expected[0].key.get_timing("Created")
    expected[2].timing["Retired"] = start
    expected[2].timing["Removed"] = expected[2].timing["Retired"] + Iret(
        config, zsk=False, ksk=True
    )
    expected[3].timing["Retired"] = start
    expected[3].timing["Removed"] = expected[3].timing["Retired"] + Iret(
        config, zsk=True, ksk=False
    )

    isctest.kasp.check_keytimes(keys, expected)
    isctest.kasp.check_dnssecstatus(ns3, zone, keys, policy=policy)
    isctest.kasp.check_apex(ns3, zone, ksks, zsks)
    isctest.kasp.check_subdomain(ns3, zone, ksks, zsks)
