# 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.

# pylint: disable=redefined-outer-name,unused-import

import os
import shutil
import time

import dns
import dns.update
import pytest

import isctest
import isctest.mark
from isctest.vars.algorithms import RSASHA1
from nsec3.common import (
    ALGORITHM,
    SIZE,
    default_config,
    pytestmark,
    check_nsec3_case,
)

# include the following zones when rendering named configs
ZONES = {
    "nsec3-to-nsec.kasp",
    "nsec-to-nsec3.kasp",
    "nsec3.kasp",
    "nsec3-dynamic.kasp",
    "nsec3-dynamic-change.kasp",
    "nsec3-dynamic-to-inline.kasp",
    "nsec3-inline-to-dynamic.kasp",
    # "nsec3-to-optout.kasp",
    # "nsec3-from-optout.kasp",
    "nsec3-other.kasp",
    "nsec3-ent.kasp",
}

if os.environ["RSASHA1_SUPPORTED"] == "1":
    ZONES.update(
        {
            "rsasha1-to-nsec3-wait.kasp",
            "nsec3-to-rsasha1.kasp",
            "nsec3-to-rsasha1-ds.kasp",
            "rsasha1-to-nsec3.kasp",
        }
    )


def bootstrap():
    return {
        "zones": ZONES,
    }


@pytest.fixture(scope="module", autouse=True)
def after_servers_start(ns3, templates):
    # First make sure all zones are properly signed. Here we specifically need
    # to wait until all zones have finished key management before we can
    # reconfigure the server, because changing the DNSSEC policy relies on
    # zones having finished applying their initial policy.
    for zone in ZONES:
        isctest.kasp.wait_keymgr_done(ns3, zone)

    # Ensure rsasha1-to-nsec3-wait.kasp is fully signed prior to reconfig.
    with_rsasha1 = "RSASHA1_SUPPORTED"
    assert with_rsasha1 in os.environ, f"{with_rsasha1} env variable undefined"
    if os.getenv(with_rsasha1) == "1":
        zone = "rsasha1-to-nsec3-wait.kasp"
        isctest.kasp.check_dnssec_verify(ns3, zone)

    # Reconfigure.
    data = {
        "reconfiged": True,
        "zones": ZONES,
    }
    templates.render(f"{ns3.identifier}/named-fips.conf", data)
    templates.render(f"{ns3.identifier}/named-rsasha1.conf", data)
    ns3.reconfigure()


@pytest.mark.parametrize(
    "params",
    [
        pytest.param(
            {
                "zone": "rsasha1-to-nsec3.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {RSASHA1.number} 2048 goal:hidden dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="rsasha1-to-nsec3.kasp",
            marks=isctest.mark.with_algorithm("RSASHA1"),
        ),
        pytest.param(
            {
                "zone": "rsasha1-to-nsec3-wait.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {RSASHA1.number} 2048 goal:hidden dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="rsasha1-to-nsec3-wait.kasp",
            marks=isctest.mark.with_algorithm("RSASHA1"),
        ),
        pytest.param(
            {
                "zone": "nsec3-to-rsasha1.kasp",
                "policy": "rsasha1",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:hidden dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
                    f"csk 0 {RSASHA1.number} 2048 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-to-rsasha1.kasp",
            marks=isctest.mark.with_algorithm("RSASHA1"),
        ),
        pytest.param(
            {
                "zone": "nsec3-to-rsasha1-ds.kasp",
                "policy": "rsasha1",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:hidden dnskey:omnipresent krrsig:omnipresent zrrsig:omnipresent ds:omnipresent",
                    f"csk 0 {RSASHA1.number} 2048 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-to-rsasha1-ds.kasp",
            marks=isctest.mark.with_algorithm("RSASHA1"),
        ),
        pytest.param(
            {
                "zone": "nsec3-to-nsec.kasp",
                "policy": "nsec",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-to-nsec.kasp",
        ),
    ],
)
def test_nsec_case(ns3, params):
    zone = params["zone"]

    # First make sure the zone is properly signed.
    isctest.kasp.wait_keymgr_done(ns3, zone, reconfig=True)

    # Test case.
    check_nsec3_case(ns3, params, nsec3=False)


@pytest.mark.parametrize(
    "params",
    [
        pytest.param(
            {
                "zone": "nsec-to-nsec3.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec-to-nsec3.kasp",
        ),
        pytest.param(
            {
                "zone": "nsec3.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3.kasp",
        ),
        pytest.param(
            {
                "zone": "nsec3-dynamic.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-dynamic.kasp",
        ),
        pytest.param(
            {
                "zone": "nsec3-dynamic-change.kasp",
                "policy": "nsec3-other",
                "nsec3param": {
                    "optout": 1,
                    "salt-length": 8,
                },
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-dynamic-change.kasp",
        ),
        pytest.param(
            {
                "zone": "nsec3-dynamic-to-inline.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-dynamic-to-inline.kasp",
        ),
        pytest.param(
            {
                "zone": "nsec3-inline-to-dynamic.kasp",
                "policy": "nsec3",
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-inline-to-dynamic.kasp",
        ),
        # DISABLED:
        # There is a bug in the nsec3param building code that thinks when the
        # optout bit is changed, the chain already exists. [GL #2216]
        # pytest.param(
        #    {
        #        "zone": "nsec3-to-optout.kasp",
        #        "policy": "nsec3",
        #        "nsec3param": {
        #            "optout": 1,
        #            "salt-length": 0,
        #        },
        #        "key-properties": [
        #            f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
        #        ],
        #    },
        #    id="nsec3-to-optout.kasp",
        # ),
        # DISABLED:
        # There is a bug in the nsec3param building code that thinks when the
        # optout bit is changed, the chain already exists. [GL #2216]
        # pytest.param(
        #    {
        #        "zone": "nsec3-from-optout.kasp",
        #        "policy": "optout",
        #        "key-properties": [
        #            f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
        #        ],
        #    },
        #    id="nsec3-from-optout.kasp",
        # ),
        pytest.param(
            {
                "zone": "nsec3-other.kasp",
                "policy": "nsec3-other",
                "nsec3param": {
                    "optout": 1,
                    "salt-length": 8,
                },
                "key-properties": [
                    f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
                ],
            },
            id="nsec3-other.kasp",
        ),
    ],
)
def test_nsec3_case(ns3, params):
    # Get test parameters.
    zone = params["zone"]

    # First make sure the zone is properly signed.
    isctest.kasp.wait_keymgr_done(ns3, zone, reconfig=True)

    # Test case.
    check_nsec3_case(ns3, params)


def test_nsec3_ent(ns3, templates):
    # Zone: nsec3-ent.kasp (regression test for #5108)
    params = {
        "zone": "nsec3-ent.kasp",
        "policy": "nsec3",
        "key-properties": [
            f"csk 0 {ALGORITHM} {SIZE} goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden",
        ],
    }

    zone = params["zone"]
    fqdn = f"{zone}."

    # First make sure the zone is properly signed.
    isctest.kasp.wait_keymgr_done(ns3, zone, reconfig=True)

    # Test case.
    check_nsec3_case(ns3, params)

    # Test empty non-terminals do not trigger a crash.
    isctest.log.info("check query for newly empty name does not crash")

    # confirm the pre-existing name still exists
    query = isctest.query.create(f"c.{fqdn}", dns.rdatatype.A)
    response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3)
    assert response.rcode() == dns.rcode.NOERROR

    match = "10.0.0.3"
    rrset = response.get_rrset(
        response.answer,
        dns.name.from_text(f"c.{fqdn}"),
        dns.rdataclass.IN,
        dns.rdatatype.A,
    )
    assert rrset is not None, "no A records found in answer section"
    assert match in str(rrset[0])

    # remove a name, bump the SOA, and reload
    time.sleep(1)
    templates.render(f"{ns3.identifier}/nsec3-ent.kasp.db", {"serial": 2})

    messages = [
        f"zone {zone}/IN (unsigned): loaded serial 2",
        f"zone_needdump: zone {zone}/IN (signed): enter",
    ]
    with ns3.watch_log_from_here() as watcher:
        ns3.rndc(f"reload {zone}")
        watcher.wait_for_sequence(messages)

    # try the query again
    query = isctest.query.create(f"c.{fqdn}", dns.rdatatype.A)
    response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3)
    assert response.rcode() == dns.rcode.NXDOMAIN

    isctest.log.info("check queries for new names below ENT do not crash")

    # confirm the ENT name does not exist yet
    query = isctest.query.create(f"x.y.z.{fqdn}", dns.rdatatype.A)
    response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3)
    assert response.rcode() == dns.rcode.NXDOMAIN

    # add a name with an ENT, bump the SOA, and reload ensuring the time stamp changes
    time.sleep(1)
    templates.render(f"{ns3.identifier}/nsec3-ent.kasp.db", {"serial": 3})

    messages = [
        f"zone {zone}/IN (unsigned): loaded serial 3",
        f"zone_needdump: zone {zone}/IN (signed): enter",
    ]
    with ns3.watch_log_from_here() as watcher:
        ns3.rndc(f"reload {zone}")
        watcher.wait_for_sequence(messages)

    # try the query again
    query = isctest.query.create(f"x.y.z.{fqdn}", dns.rdatatype.A)
    response = isctest.query.tcp(query, ns3.ip, ns3.ports.dns, timeout=3)
    assert response.rcode() == dns.rcode.NOERROR

    match = "10.0.0.4"
    rrset = response.get_rrset(
        response.answer,
        dns.name.from_text(f"x.y.z.{fqdn}"),
        dns.rdataclass.IN,
        dns.rdatatype.A,
    )
    assert rrset is not None, "no A records found in answer section"
    assert match in str(rrset[0])
