# 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 typing import SupportsInt

import socket

import dns.flags
import dns.name
import dns.opcode
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.rdtypes.ANY.NSEC3
import dns.rdtypes.ANY.OPT
import dns.rdtypes.ANY.RRSIG
import dns.rdtypes.ANY.SOA
import dns.rdtypes.ANY.TSIG
import dns.rdtypes.IN.A
import pytest


def wire(*parts: bytes) -> bytes:
    return b"".join(parts)


def u16(value: SupportsInt) -> bytes:
    return int(value).to_bytes(2, byteorder="big")


def u32(value: SupportsInt) -> bytes:
    return int(value).to_bytes(4, byteorder="big")


def name(text: str) -> bytes:
    return dns.name.from_text(text).to_wire()


def root() -> bytes:
    return dns.name.root.to_wire()


def header(
    message_id: int = 0,
    flags: int = 0,
    opcode: int = dns.opcode.QUERY,
    rcode: dns.rcode.Rcode = dns.rcode.NOERROR,
    qdcount: int = 0,
    ancount: int = 0,
    nscount: int = 0,
    arcount: int = 0,
) -> bytes:
    return wire(
        u16(message_id),
        u16(flags | dns.opcode.to_flags(opcode) | int(rcode)),
        u16(qdcount),
        u16(ancount),
        u16(nscount),
        u16(arcount),
    )


def formerr_response_header(
    message_id: int = 0,
    opcode: int = dns.opcode.QUERY,
    rcode: dns.rcode.Rcode = dns.rcode.FORMERR,
    qdcount: int = 0,
    ancount: int = 0,
    nscount: int = 0,
    arcount: int = 0,
) -> bytes:
    return header(
        message_id=message_id,
        flags=dns.flags.QR,
        opcode=opcode,
        rcode=rcode,
        qdcount=qdcount,
        ancount=ancount,
        nscount=nscount,
        arcount=arcount,
    )


def question(
    qname: bytes,
    qtype: dns.rdatatype.RdataType = dns.rdatatype.RdataType.A,
    qclass: dns.rdataclass.RdataClass = dns.rdataclass.RdataClass.IN,
) -> bytes:
    return wire(
        qname,
        u16(qtype),
        u16(qclass),
    )


def rr(
    owner: bytes,
    rrtype: SupportsInt,
    rrclass: SupportsInt,
    *,
    ttl: int = 1,
    rdata: bytes = b"",
) -> bytes:
    return wire(
        question(
            owner, dns.rdatatype.RdataType(rrtype), dns.rdataclass.RdataClass(rrclass)
        ),
        u32(ttl),
        u16(len(rdata)),
        rdata,
    )


def oversized_name() -> bytes:
    labels = [bytes([15]) + b"A" * 15 for _ in range(15)]
    labels.append(bytes([14]) + b"A" * 14 + root())
    return wire(*labels)


def soa_rr(
    *,
    minimum: int,
) -> bytes:
    return rr(
        root(),
        dns.rdatatype.RdataType.SOA,
        dns.rdataclass.RdataClass.IN,
        rdata=dns.rdtypes.ANY.SOA.SOA(
            dns.rdataclass.RdataClass.IN,
            dns.rdatatype.RdataType.SOA,
            dns.name.root,
            dns.name.root,
            1,
            2,
            3,
            4,
            minimum,
        ).to_wire(),
    )


def nsec3_rr(
    *,
    owner: bytes,
) -> bytes:
    return rr(
        owner,
        dns.rdatatype.RdataType.NSEC3,
        dns.rdataclass.RdataClass.IN,
        rdata=dns.rdtypes.ANY.NSEC3.NSEC3(
            dns.rdataclass.RdataClass.IN,
            dns.rdatatype.RdataType.NSEC3,
            algorithm=240,
            flags=0,
            iterations=0,
            salt=b"",
            next=b"\xff",
            windows=[],
        ).to_wire(),
    )


def key_rdata(
    *,
    flags: int,
    protocol: int,
    algorithm: int,
    keydata: bytes,
) -> bytes:
    # No dns.rdtypes.ANY.KEY.KEY class, so construct the rdata manually
    return wire(u16(flags), bytes([protocol, algorithm]), keydata)


def key_rr(*, rdclass: dns.rdataclass.RdataClass) -> bytes:
    return rr(
        root(),
        dns.rdatatype.RdataType.KEY,
        rdclass,
        rdata=key_rdata(
            flags=0,
            protocol=0,
            algorithm=248,
            keydata=b"\x00",
        ),
    )


def malformed_rrsig_rr() -> bytes:
    return rr(
        root(),
        dns.rdatatype.RdataType.RRSIG,
        dns.rdataclass.RdataClass.IN,
        rdata=dns.rdtypes.ANY.RRSIG.RRSIG(
            dns.rdataclass.RdataClass.IN,
            dns.rdatatype.RdataType.RRSIG,
            0,
            240,
            0,
            1,
            2,
            3,
            0,
            dns.name.root,
            b"\x00",
        ).to_wire(),
    )


def tsig_rr(
    *,
    owner: bytes = root(),
    rdclass: dns.rdataclass.RdataClass = dns.rdataclass.RdataClass.ANY,
    algorithm: dns.name.Name = dns.name.root,
    time_signed: int = 0x010203040506,
    fudge: int = 0x0102,
    mac: bytes = b"\x00",
    original_id: int = 0,
    error: int = 0,
    other: bytes = b"",
) -> bytes:
    return rr(
        owner,
        dns.rdatatype.RdataType.TSIG,
        rdclass,
        rdata=dns.rdtypes.ANY.TSIG.TSIG(
            rdclass,
            dns.rdatatype.RdataType.TSIG,
            algorithm,
            time_signed,
            fudge,
            mac,
            original_id,
            error,
            other,
        ).to_wire(),
    )


def opt_rr(*, owner: bytes) -> bytes:
    return rr(
        owner,
        dns.rdatatype.RdataType.OPT,
        dns.rdataclass.RdataClass.IN,
        ttl=0,
        rdata=dns.rdtypes.ANY.OPT.OPT(
            dns.rdataclass.RdataClass.IN,
            dns.rdatatype.RdataType.OPT,
            [],
        ).to_wire(),
    )


def a_rdata(ipv4_bytes: bytes = b"\x00\x00\x00\x00") -> bytes:
    return dns.rdtypes.IN.A.A(
        dns.rdataclass.RdataClass.IN,
        dns.rdatatype.RdataType.A,
        ipv4_bytes,
    ).to_wire()


def a_rr(owner: bytes = root()) -> bytes:
    return rr(
        owner,
        dns.rdatatype.RdataType.A,
        dns.rdataclass.RdataClass.IN,
        rdata=a_rdata(),
    )


def query_raw_tcp(host: str, port: int, packet_wire: bytes) -> bytes:
    with (
        socket.create_connection((host, port), timeout=10) as sock,
        sock.makefile("rwb") as f,
    ):
        f.write(u16(len(packet_wire)))
        f.write(packet_wire)
        f.flush()
        length = int.from_bytes(f.read(2), byteorder="big")
        return f.read(length)


@pytest.mark.parametrize(
    "query_wire,expected_wire",
    [
        pytest.param(
            wire(
                header(qdcount=1),
                question(oversized_name()),
            ),
            formerr_response_header(),
            id="nametoolong",
        ),
        pytest.param(
            wire(
                header(qdcount=2),
                question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
                # Two names concatenated in the QNAME field
                question(
                    wire(name("AAAAAAAAAAAAAA."), name("AAAAAAAAAAAAAB.")),
                    dns.rdatatype.RdataType.A,
                ),
            ),
            formerr_response_header(),
            id="twoquestionnames",
        ),
        pytest.param(
            wire(
                header(qdcount=2),
                question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
                question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.NS),
            ),
            formerr_response_header(),
            id="twoquestiontypes",
        ),
        pytest.param(
            wire(
                header(qdcount=2),
                question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
                question(name("AAAAAAAAAAAAAA."), dns.rdatatype.RdataType.A),
            ),
            formerr_response_header(),
            id="dupquestion",
        ),
        pytest.param(
            wire(
                header(qdcount=1, ancount=2),
                question(root(), dns.rdatatype.RdataType.SOA),
                soa_rr(minimum=5),
                soa_rr(minimum=6),
            ),
            wire(
                formerr_response_header(qdcount=1),
                question(root(), dns.rdatatype.RdataType.SOA),
            ),
            id="dupans",
        ),
        pytest.param(
            wire(
                header(ancount=1),
                rr(
                    root(),
                    dns.rdatatype.RdataType.MAILB,
                    dns.rdataclass.RdataClass.IN,
                ),
            ),
            formerr_response_header(),
            id="qtypeasanswer",
        ),
        pytest.param(
            header(),
            # This would be NOERROR if it included a COOKIE option,
            # but is a FORMERR without one.
            formerr_response_header(),
            id="noquestions",
        ),
        pytest.param(
            wire(
                header(qdcount=1, nscount=1),
                question(root(), dns.rdatatype.RdataType.A),
                # Bad NSEC3 owner: X. is not in the base32hex alphabet.
                nsec3_rr(owner=name("X.")),
            ),
            wire(
                formerr_response_header(rcode=dns.rcode.SERVFAIL, qdcount=1),
                question(root(), dns.rdatatype.RdataType.A),
            ),
            id="badnsec3owner",
        ),
        pytest.param(
            wire(
                header(arcount=1),
                # Truncated A record (no ttl, length or data)
                question(root(), dns.rdatatype.RdataType.A),
            ),
            formerr_response_header(),
            id="shortrecord",
        ),
        pytest.param(
            wire(
                header(qdcount=1),
                # Truncated question (no class)
                root(),
                u16(dns.rdatatype.RdataType.A),
            ),
            formerr_response_header(),
            id="shortquestion",
        ),
        pytest.param(
            wire(
                header(qdcount=2),
                question(
                    root(),
                    dns.rdatatype.RdataType.A,
                    dns.rdataclass.RdataClass.IN,
                ),
                question(
                    root(),
                    dns.rdatatype.RdataType.A,
                    dns.rdataclass.RdataClass(2),
                ),
            ),
            formerr_response_header(),
            id="twoquestionclasses",
        ),
        pytest.param(
            wire(
                header(arcount=1),
                a_rr(owner=oversized_name()),
            ),
            formerr_response_header(),
            id="badrecordname",
        ),
        pytest.param(
            wire(
                header(arcount=2),
                a_rr(),
                rr(
                    root(),
                    dns.rdatatype.RdataType(65280),
                    dns.rdataclass.RdataClass(256),
                    rdata=a_rdata(),
                ),
            ),
            formerr_response_header(),
            id="wrongclass",
        ),
        pytest.param(
            wire(
                header(qdcount=1, arcount=1),
                question(root(), dns.rdatatype.RdataType.A),
                key_rr(rdclass=dns.rdataclass.RdataClass(2)),
            ),
            wire(
                formerr_response_header(qdcount=1),
                question(root(), dns.rdatatype.RdataType.A),
            ),
            id="keyclass",
        ),
        pytest.param(
            wire(
                header(arcount=1),
                # OPT owner should be root
                opt_rr(owner=name("A.")),
            ),
            formerr_response_header(),
            id="optwrongname",
        ),
        pytest.param(
            wire(
                header(arcount=1),
                malformed_rrsig_rr(),
            ),
            formerr_response_header(),
            id="malformedrrsig",
        ),
        pytest.param(
            wire(
                header(opcode=dns.opcode.UPDATE, nscount=1),
                rr(
                    root(),
                    dns.rdatatype.RdataType.A,
                    dns.rdataclass.RdataClass.ANY,
                    ttl=0,
                    # Non-empty rdata for DELETE type
                    rdata=b"\x00",
                ),
            ),
            formerr_response_header(opcode=dns.opcode.UPDATE),
            id="malformeddeltype",
        ),
        pytest.param(
            wire(
                header(arcount=1),
                # Class should be ANY not IN
                tsig_rr(rdclass=dns.rdataclass.RdataClass.IN),
            ),
            formerr_response_header(),
            id="tsigwrongclass",
        ),
        pytest.param(
            wire(
                # Test non-zero message ID is preserved in the response
                header(message_id=67, arcount=2),
                tsig_rr(),
                # TSIG should be the last record
                a_rr(),
            ),
            formerr_response_header(message_id=67),
            id="tsignotlast",
        ),
    ],
)
def test_formerr(
    query_wire: bytes,
    expected_wire: bytes,
    named_port: int,
    ns1,
) -> None:
    response_wire = query_raw_tcp(ns1.ip, named_port, query_wire)
    assert response_wire == expected_wire
