// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dnsmessage

import (
	"bytes"
	"math"
	"reflect"
	"testing"
)

func TestSVCBParamsRoundTrip(t *testing.T) {
	testSVCBParam := func(t *testing.T, p *SVCParam) {
		t.Helper()
		rr := &SVCBResource{
			Priority: 1,
			Target:   MustNewName("svc.example.com."),
			Params:   []SVCParam{*p},
		}
		buf, err := rr.pack([]byte{}, nil, 0)
		if err != nil {
			t.Fatalf("pack() = %v", err)
		}
		got, n, err := unpackResourceBody(buf, 0, ResourceHeader{Type: TypeSVCB, Length: uint16(len(buf))})
		if err != nil {
			t.Fatalf("unpackResourceBody() = %v", err)
		}
		if n != len(buf) {
			t.Fatalf("unpacked different amount than packed: got = %d, want = %d", n, len(buf))
		}
		if !reflect.DeepEqual(got, rr) {
			t.Fatalf("roundtrip mismatch: got = %#v, want = %#v", got, rr)
		}
	}

	testSVCBParam(t, &SVCParam{Key: SVCParamMandatory, Value: []byte{0x00, 0x01, 0x00, 0x03, 0x00, 0x05}})
	testSVCBParam(t, &SVCParam{Key: SVCParamALPN, Value: []byte{0x02, 'h', '2', 0x02, 'h', '3'}})
	testSVCBParam(t, &SVCParam{Key: SVCParamNoDefaultALPN, Value: []byte{}})
	testSVCBParam(t, &SVCParam{Key: SVCParamPort, Value: []byte{0x1f, 0x90}}) // 8080
	testSVCBParam(t, &SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1, 198, 51, 100, 2}})
	testSVCBParam(t, &SVCParam{Key: SVCParamECH, Value: []byte{0x01, 0x02, 0x03, 0x04}})
	testSVCBParam(t, &SVCParam{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}})
	testSVCBParam(t, &SVCParam{Key: SVCParamDOHPath, Value: []byte("/dns-query{?dns}")})
	testSVCBParam(t, &SVCParam{Key: SVCParamOHTTP, Value: []byte{0x00, 0x01, 0x02, 0x03}})
	testSVCBParam(t, &SVCParam{Key: SVCParamTLSSupportedGroups, Value: []byte{0x00, 0x1d, 0x00, 0x17}})
}

func TestSVCBParsingAllocs(t *testing.T) {
	name := MustNewName("foo.bar.example.com.")
	msg := Message{
		Header:    Header{Response: true, Authoritative: true},
		Questions: []Question{{Name: name, Type: TypeA, Class: ClassINET}},
		Answers: []Resource{{
			Header: ResourceHeader{Name: name, Type: TypeSVCB, Class: ClassINET, TTL: 300},
			Body: &SVCBResource{
				Priority: 1,
				Target:   MustNewName("svc.example.com."),
				Params: []SVCParam{
					{Key: SVCParamMandatory, Value: []byte{0x00, 0x01, 0x00, 0x03, 0x00, 0x05}},
					{Key: SVCParamALPN, Value: []byte{0x02, 'h', '2', 0x02, 'h', '3'}},
					{Key: SVCParamPort, Value: []byte{0x1f, 0x90}}, // 8080
					{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1, 198, 51, 100, 2}},
					{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}},
				},
			},
		}},
	}
	buf, err := msg.Pack()
	if err != nil {
		t.Fatal(err)
	}

	allocs := int(testing.AllocsPerRun(1, func() {
		var p Parser
		if _, err := p.Start(buf); err != nil {
			t.Fatal("Parser.Start(non-nil) =", err)
		}
		if err := p.SkipAllQuestions(); err != nil {
			t.Fatal("Parser.SkipAllQuestions(non-nil) =", err)
		}
		if _, err = p.AnswerHeader(); err != nil {
			t.Fatal("Parser.AnswerHeader(non-nil) =", err)
		}
		if _, err = p.SVCBResource(); err != nil {
			t.Fatal("Parser.SVCBResource(non-nil) =", err)
		}
	}))

	// Make sure we have only two allocations: one for the SVCBResource.Params slice, and one
	// for the SVCParam Values.
	if allocs != 2 {
		t.Errorf("allocations during parsing: got = %d, want 2", allocs)
	}
}

func TestHTTPSBuildAllocs(t *testing.T) {
	b := NewBuilder([]byte{}, Header{Response: true, Authoritative: true})
	b.EnableCompression()
	if err := b.StartQuestions(); err != nil {
		t.Fatalf("StartQuestions() = %v", err)
	}
	if err := b.Question(Question{Name: MustNewName("foo.bar.example.com."), Type: TypeHTTPS, Class: ClassINET}); err != nil {
		t.Fatalf("Question() = %v", err)
	}
	if err := b.StartAnswers(); err != nil {
		t.Fatalf("StartAnswers() = %v", err)
	}

	header := ResourceHeader{Name: MustNewName("foo.bar.example.com."), Type: TypeHTTPS, Class: ClassINET, TTL: 300}
	resource := HTTPSResource{SVCBResource{Priority: 1, Target: MustNewName("svc.example.com.")}}

	// AllocsPerRun runs the function once to "warm up" before running the measurement.
	// So technically this function is running twice, on different data, which can potentially
	// make the measurement inaccurate (e.g. by using the name cache the second time).
	// So we make sure we don't run in the warm-up phase.
	warmUp := true
	allocs := int(testing.AllocsPerRun(1, func() {
		if warmUp {
			warmUp = false
			return
		}
		if err := b.HTTPSResource(header, resource); err != nil {
			t.Fatalf("HTTPSResource() = %v", err)
		}
	}))
	if allocs != 1 {
		t.Fatalf("unexpected allocations: got = %d, want = 1", allocs)
	}
}

func TestSVCBParams(t *testing.T) {
	rr := SVCBResource{Priority: 1, Target: MustNewName("svc.example.com.")}
	if _, ok := rr.GetParam(SVCParamALPN); ok {
		t.Fatal("GetParam found non-existent param")
	}
	rr.SetParam(SVCParamIPv4Hint, []byte{192, 0, 2, 1})
	inALPN := []byte{0x02, 'h', '2', 0x02, 'h', '3'}
	rr.SetParam(SVCParamALPN, inALPN)

	// Check sorting of params
	packed, err := rr.pack([]byte{}, nil, 0)
	if err != nil {
		t.Fatal("pack() =", err)
	}
	expectedBytes := []byte{
		0x00, 0x01, // priority
		0x03, 0x73, 0x76, 0x63, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
		0x00, 0x01, // key 1
		0x00, 0x06, // length 6
		0x02, 'h', '2', 0x02, 'h', '3', // value
		0x00, 0x04, // key 4
		0x00, 0x04, // length 4
		192, 0, 2, 1, // value
	}
	if !reflect.DeepEqual(packed, expectedBytes) {
		t.Fatalf("pack() produced unexpected output: want = %v, got = %v", expectedBytes, packed)
	}

	// Check GetParam and DeleteParam.
	if outALPN, ok := rr.GetParam(SVCParamALPN); !ok || !bytes.Equal(outALPN, inALPN) {
		t.Fatal("GetParam failed to retrieve set param")
	}
	if !rr.DeleteParam(SVCParamALPN) {
		t.Fatal("DeleteParam failed to remove existing param")
	}
	if _, ok := rr.GetParam(SVCParamALPN); ok {
		t.Fatal("GetParam found deleted param")
	}
	if len(rr.Params) != 1 || rr.Params[0].Key != SVCParamIPv4Hint {
		t.Fatalf("DeleteParam removed wrong param: got = %#v, want = [%#v]", rr.Params, SVCParam{Key: SVCParamIPv4Hint, Value: []byte{192, 0, 2, 1}})
	}
}

func TestSVCBWireFormat(t *testing.T) {
	testRecord := func(bytesInput []byte, parsedInput *SVCBResource) {
		parsedOutput, n, err := unpackResourceBody(bytesInput, 0, ResourceHeader{Type: TypeSVCB, Length: uint16(len(bytesInput))})
		if err != nil {
			t.Fatalf("unpackResourceBody() = %v", err)
		}
		if n != len(bytesInput) {
			t.Fatalf("unpacked different amount than packed: got = %d, want = %d", n, len(bytesInput))
		}
		if !reflect.DeepEqual(parsedOutput, parsedInput) {
			t.Fatalf("unpack mismatch: got = %#v, want = %#v", parsedOutput, parsedInput)
		}

		bytesOutput, err := parsedInput.pack([]byte{}, nil, 0)
		if err != nil {
			t.Fatalf("pack() = %v", err)
		}
		if !reflect.DeepEqual(bytesOutput, bytesInput) {
			t.Fatalf("pack mismatch: got = %#v, want = %#v", bytesOutput, bytesInput)
		}
	}
	// Test examples from https://datatracker.ietf.org/doc/html/rfc9460#name-test-vectors

	// Example D.1. Alias Mode

	// Figure 2: AliasMode
	// example.com.   HTTPS   0 foo.example.com.
	bytes := []byte{
		0x00, 0x00, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target: foo.example.com.
	}
	parsed := &SVCBResource{
		Priority: 0,
		Target:   MustNewName("foo.example.com."),
		Params:   []SVCParam{},
	}
	testRecord(bytes, parsed)

	// Example D.2. Service Mode

	// Figure 3: TargetName Is "."
	// example.com.   SVCB   1 .
	bytes = []byte{
		0x00, 0x01, // priority
		0x00, // target (root label)
	}
	parsed = &SVCBResource{
		Priority: 1,
		Target:   MustNewName("."),
		Params:   []SVCParam{},
	}
	testRecord(bytes, parsed)

	// Figure 4: Specifies a Port
	// example.com.   SVCB   16 foo.example.com. port=53
	bytes = []byte{
		0x00, 0x10, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
		0x00, 0x03, // key 3
		0x00, 0x02, // length 2
		0x00, 0x35, // value
	}
	parsed = &SVCBResource{
		Priority: 16,
		Target:   MustNewName("foo.example.com."),
		Params:   []SVCParam{{Key: SVCParamPort, Value: []byte{0x00, 0x35}}},
	}
	testRecord(bytes, parsed)

	// Figure 5: A Generic Key and Unquoted Value
	// example.com.   SVCB   1 foo.example.com. key667=hello
	bytes = []byte{
		0x00, 0x01, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
		0x02, 0x9b, // key 667
		0x00, 0x05, // length 5
		0x68, 0x65, 0x6c, 0x6c, 0x6f, // value
	}
	parsed = &SVCBResource{
		Priority: 1,
		Target:   MustNewName("foo.example.com."),
		Params:   []SVCParam{{Key: 667, Value: []byte("hello")}},
	}
	testRecord(bytes, parsed)

	// Figure 6: A Generic Key and Quoted Value with a Decimal Escape
	// example.com.   SVCB   1 foo.example.com. key667="hello\210qoo"
	bytes = []byte{
		0x00, 0x01, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
		0x02, 0x9b, // key 667
		0x00, 0x09, // length 9
		0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2, 0x71, 0x6f, 0x6f, // value
	}
	parsed = &SVCBResource{
		Priority: 1,
		Target:   MustNewName("foo.example.com."),
		Params:   []SVCParam{{Key: 667, Value: []byte("hello\xd2qoo")}},
	}
	testRecord(bytes, parsed)

	// Figure 7: Two Quoted IPv6 Hints
	// example.com.   SVCB   1 foo.example.com. (
	//                       ipv6hint="2001:db8::1,2001:db8::53:1"
	//                       )
	bytes = []byte{
		0x00, 0x01, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
		0x00, 0x06, // key 6
		0x00, 0x20, // length 32
		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // first address
		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x01, // second address
	}
	parsed = &SVCBResource{
		Priority: 1,
		Target:   MustNewName("foo.example.com."),
		Params:   []SVCParam{{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x01}}},
	}
	testRecord(bytes, parsed)

	// Figure 8: An IPv6 Hint Using the Embedded IPv4 Syntax
	// example.com.   SVCB   1 example.com. (
	//                        ipv6hint="2001:db8:122:344::192.0.2.33"
	//                        )
	bytes = []byte{
		0x00, 0x01, // priority
		0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, // target
		0x00, 0x06, // key 6
		0x00, 0x10, // length 16
		0x20, 0x01, 0x0d, 0xb8, 0x01, 0x22, 0x03, 0x44, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x21, // address
	}
	parsed = &SVCBResource{
		Priority: 1,
		Target:   MustNewName("example.com."),
		Params:   []SVCParam{{Key: SVCParamIPv6Hint, Value: []byte{0x20, 0x01, 0x0d, 0xb8, 0x01, 0x22, 0x03, 0x44, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x02, 0x21}}},
	}
	testRecord(bytes, parsed)

	// Figure 9: SvcParamKey Ordering Is Arbitrary in Presentation Format but Sorted in Wire Format
	// example.com.   SVCB   16 foo.example.org. (
	//                      alpn=h2,h3-19 mandatory=ipv4hint,alpn
	//                      ipv4hint=192.0.2.1
	//                      )
	bytes = []byte{
		0x00, 0x10, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x6f, 0x72, 0x67, 0x00, // target
		0x00, 0x00, // key 0
		0x00, 0x04, // param length 4
		0x00, 0x01, // value: key 1
		0x00, 0x04, // value: key 4
		0x00, 0x01, // key 1
		0x00, 0x09, // param length 9
		0x02,       // alpn length 2
		0x68, 0x32, // alpn value
		0x05,                         // alpn length 5
		0x68, 0x33, 0x2d, 0x31, 0x39, // alpn value
		0x00, 0x04, // key 4
		0x00, 0x04, // param length 4
		0xc0, 0x00, 0x02, 0x01, // param value
	}
	parsed = &SVCBResource{
		Priority: 16,
		Target:   MustNewName("foo.example.org."),
		Params: []SVCParam{
			{Key: SVCParamMandatory, Value: []byte{0x00, 0x01, 0x00, 0x04}},
			{Key: SVCParamALPN, Value: []byte{0x02, 0x68, 0x32, 0x05, 0x68, 0x33, 0x2d, 0x31, 0x39}},
			{Key: SVCParamIPv4Hint, Value: []byte{0xc0, 0x00, 0x02, 0x01}},
		},
	}
	testRecord(bytes, parsed)

	// Figure 10: An "alpn" Value with an Escaped Comma and an Escaped Backslash in Two Presentation Formats
	// example.com.   SVCB   16 foo.example.org. alpn=f\\\092oo\092,bar,h2
	bytes = []byte{
		0x00, 0x10, // priority
		0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x6f, 0x72, 0x67, 0x00, // target
		0x00, 0x01, // key 1
		0x00, 0x0c, // param length 12
		0x08,                                           // alpn length 8
		0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, // alpn value
		0x02,       // alpn length 2
		0x68, 0x32, // alpn value
	}
	parsed = &SVCBResource{
		Priority: 16,
		Target:   MustNewName("foo.example.org."),
		Params: []SVCParam{
			{Key: SVCParamALPN, Value: []byte{0x08, 0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, 0x02, 0x68, 0x32}},
		},
	}
	testRecord(bytes, parsed)
}

func TestSVCBPackLongValue(t *testing.T) {
	b := NewBuilder(nil, Header{})
	b.StartQuestions()
	b.StartAnswers()

	res := SVCBResource{
		Target: MustNewName("example.com."),
		Params: []SVCParam{
			{
				Key:   SVCParamMandatory,
				Value: make([]byte, math.MaxUint16+1),
			},
		},
	}

	err := b.SVCBResource(ResourceHeader{Name: MustNewName("example.com.")}, res)
	if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" {
		t.Fatalf(`b.SVCBResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err)
	}

	err = b.HTTPSResource(ResourceHeader{Name: MustNewName("example.com.")}, HTTPSResource{res})
	if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" {
		t.Fatalf(`b.HTTPSResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err)
	}
}
