// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2022 mochi-co
// SPDX-FileContributor: mochi-co

package packets

import (
	"bytes"
	"testing"

	"github.com/stretchr/testify/require"
)

var (
	propertiesStruct = Properties{
		PayloadFormat:             byte(1), // UTF-8 Format
		PayloadFormatFlag:         true,
		MessageExpiryInterval:     uint32(2),
		ContentType:               "text/plain",
		ResponseTopic:             "a/b/c",
		CorrelationData:           []byte("data"),
		SubscriptionIdentifier:    []int{322122},
		SessionExpiryInterval:     uint32(120),
		SessionExpiryIntervalFlag: true,
		AssignedClientID:          "mochi-v5",
		ServerKeepAlive:           uint16(20),
		ServerKeepAliveFlag:       true,
		AuthenticationMethod:      "SHA-1",
		AuthenticationData:        []byte("auth-data"),
		RequestProblemInfo:        byte(1),
		RequestProblemInfoFlag:    true,
		WillDelayInterval:         uint32(600),
		RequestResponseInfo:       byte(1),
		ResponseInfo:              "response",
		ServerReference:           "mochi-2",
		ReasonString:              "reason",
		ReceiveMaximum:            uint16(500),
		TopicAliasMaximum:         uint16(999),
		TopicAlias:                uint16(3),
		TopicAliasFlag:            true,
		MaximumQos:                byte(1),
		MaximumQosFlag:            true,
		RetainAvailable:           byte(1),
		RetainAvailableFlag:       true,
		User: []UserProperty{
			{
				Key: "hello",
				Val: "世界",
			},
			{
				Key: "key2",
				Val: "value2",
			},
		},
		MaximumPacketSize:        uint32(32000),
		WildcardSubAvailable:     byte(1),
		WildcardSubAvailableFlag: true,
		SubIDAvailable:           byte(1),
		SubIDAvailableFlag:       true,
		SharedSubAvailable:       byte(1),
		SharedSubAvailableFlag:   true,
	}

	propertiesBytes = []byte{
		172, 1, // VBI

		// Payload Format (1) (vbi:2)
		1, 1,

		// Message Expiry (2) (vbi:7)
		2, 0, 0, 0, 2,

		// Content Type (3) (vbi:20)
		3,
		0, 10, 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',

		// Response Topic (8) (vbi:28)
		8,
		0, 5, 'a', '/', 'b', '/', 'c',

		// Correlations Data (9) (vbi:35)
		9,
		0, 4, 'd', 'a', 't', 'a',

		// Subscription Identifier (11) (vbi:39)
		11,
		202, 212, 19,

		// Session Expiry Interval (17) (vbi:43)
		17,
		0, 0, 0, 120,

		// Assigned Client ID (18) (vbi:55)
		18,
		0, 8, 'm', 'o', 'c', 'h', 'i', '-', 'v', '5',

		// Server Keep Alive (19) (vbi:58)
		19,
		0, 20,

		// Authentication Method (21) (vbi:66)
		21,
		0, 5, 'S', 'H', 'A', '-', '1',

		// Authentication Data (22) (vbi:78)
		22,
		0, 9, 'a', 'u', 't', 'h', '-', 'd', 'a', 't', 'a',

		// Request Problem Info (23) (vbi:80)
		23, 1,

		// Will Delay Interval (24) (vbi:85)
		24,
		0, 0, 2, 88,

		// Request Response Info (25) (vbi:87)
		25, 1,

		// Response Info (26) (vbi:98)
		26,
		0, 8, 'r', 'e', 's', 'p', 'o', 'n', 's', 'e',

		// Server Reference (28) (vbi:108)
		28,
		0, 7, 'm', 'o', 'c', 'h', 'i', '-', '2',

		// Reason String (31) (vbi:117)
		31,
		0, 6, 'r', 'e', 'a', 's', 'o', 'n',

		// Receive Maximum (33) (vbi:120)
		33,
		1, 244,

		// Topic Alias Maximum (34) (vbi:123)
		34,
		3, 231,

		// Topic Alias (35) (vbi:126)
		35,
		0, 3,

		// Maximum Qos (36) (vbi:128)
		36, 1,

		// Retain Available (37) (vbi: 130)
		37, 1,

		// User Properties (38) (vbi:161)
		38,
		0, 5, 'h', 'e', 'l', 'l', 'o',
		0, 6, 228, 184, 150, 231, 149, 140,
		38,
		0, 4, 'k', 'e', 'y', '2',
		0, 6, 'v', 'a', 'l', 'u', 'e', '2',

		// Maximum Packet Size (39) (vbi:166)
		39,
		0, 0, 125, 0,

		// Wildcard Subscriptions Available (40)  (vbi:168)
		40, 1,

		// Subscription ID Available (41) (vbi:170)
		41, 1,

		// Shared Subscriptions Available (42) (vbi:172)
		42, 1,
	}
)

func init() {
	validPacketProperties[PropPayloadFormat][Reserved] = 1
	validPacketProperties[PropMessageExpiryInterval][Reserved] = 1
	validPacketProperties[PropContentType][Reserved] = 1
	validPacketProperties[PropResponseTopic][Reserved] = 1
	validPacketProperties[PropCorrelationData][Reserved] = 1
	validPacketProperties[PropSubscriptionIdentifier][Reserved] = 1
	validPacketProperties[PropSessionExpiryInterval][Reserved] = 1
	validPacketProperties[PropAssignedClientID][Reserved] = 1
	validPacketProperties[PropServerKeepAlive][Reserved] = 1
	validPacketProperties[PropAuthenticationMethod][Reserved] = 1
	validPacketProperties[PropAuthenticationData][Reserved] = 1
	validPacketProperties[PropRequestProblemInfo][Reserved] = 1
	validPacketProperties[PropWillDelayInterval][Reserved] = 1
	validPacketProperties[PropRequestResponseInfo][Reserved] = 1
	validPacketProperties[PropResponseInfo][Reserved] = 1
	validPacketProperties[PropServerReference][Reserved] = 1
	validPacketProperties[PropReasonString][Reserved] = 1
	validPacketProperties[PropReceiveMaximum][Reserved] = 1
	validPacketProperties[PropTopicAliasMaximum][Reserved] = 1
	validPacketProperties[PropTopicAlias][Reserved] = 1
	validPacketProperties[PropMaximumQos][Reserved] = 1
	validPacketProperties[PropRetainAvailable][Reserved] = 1
	validPacketProperties[PropUser][Reserved] = 1
	validPacketProperties[PropMaximumPacketSize][Reserved] = 1
	validPacketProperties[PropWildcardSubAvailable][Reserved] = 1
	validPacketProperties[PropSubIDAvailable][Reserved] = 1
	validPacketProperties[PropSharedSubAvailable][Reserved] = 1
}

func TestEncodeProperties(t *testing.T) {
	props := propertiesStruct
	b := bytes.NewBuffer([]byte{})
	props.Encode(&Packet{FixedHeader: FixedHeader{Type: Reserved}, Mods: Mods{AllowResponseInfo: true}}, b, 0)
	require.Equal(t, propertiesBytes, b.Bytes())
}

func TestEncodePropertiesDisallowProblemInfo(t *testing.T) {
	props := propertiesStruct
	b := bytes.NewBuffer([]byte{})
	props.Encode(&Packet{FixedHeader: FixedHeader{Type: Reserved}, Mods: Mods{DisallowProblemInfo: true}}, b, 0)
	require.NotEqual(t, propertiesBytes, b.Bytes())
	require.False(t, bytes.Contains(b.Bytes(), []byte{31, 0, 6}))
	require.False(t, bytes.Contains(b.Bytes(), []byte{38, 0, 5}))
	require.False(t, bytes.Contains(b.Bytes(), []byte{26, 0, 8}))
}

func TestEncodePropertiesDisallowResponseInfo(t *testing.T) {
	props := propertiesStruct
	b := bytes.NewBuffer([]byte{})
	props.Encode(&Packet{FixedHeader: FixedHeader{Type: Reserved}, Mods: Mods{AllowResponseInfo: false}}, b, 0)
	require.NotEqual(t, propertiesBytes, b.Bytes())
	require.NotContains(t, b.Bytes(), []byte{8, 0, 5})
	require.NotContains(t, b.Bytes(), []byte{9, 0, 4})
}

func TestEncodePropertiesNil(t *testing.T) {
	type tmp struct {
		p *Properties
	}

	pr := tmp{}
	b := bytes.NewBuffer([]byte{})
	pr.p.Encode(&Packet{FixedHeader: FixedHeader{Type: Reserved}}, b, 0)
	require.Equal(t, []byte{}, b.Bytes())
}

func TestEncodeZeroProperties(t *testing.T) {
	// [MQTT-2.2.2-1] If there are no properties, this MUST be indicated by including a Property Length of zero.
	props := new(Properties)
	b := bytes.NewBuffer([]byte{})
	props.Encode(&Packet{FixedHeader: FixedHeader{Type: Reserved}, Mods: Mods{AllowResponseInfo: true}}, b, 0)
	require.Equal(t, []byte{0x00}, b.Bytes())
}

func TestDecodeProperties(t *testing.T) {
	b := bytes.NewBuffer(propertiesBytes)

	props := new(Properties)
	n, err := props.Decode(Reserved, b)
	require.NoError(t, err)
	require.Equal(t, 172+2, n)
	require.EqualValues(t, propertiesStruct, *props)
}

func TestDecodePropertiesNil(t *testing.T) {
	b := bytes.NewBuffer(propertiesBytes)

	type tmp struct {
		p *Properties
	}

	pr := tmp{}
	n, err := pr.p.Decode(Reserved, b)
	require.NoError(t, err)
	require.Equal(t, 0, n)
}

func TestDecodePropertiesBadInitialVBI(t *testing.T) {
	b := bytes.NewBuffer([]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.Error(t, err)
	require.ErrorIs(t, ErrMalformedVariableByteInteger, err)
}

func TestDecodePropertiesZeroLengthVBI(t *testing.T) {
	b := bytes.NewBuffer([]byte{0})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.NoError(t, err)
	require.Equal(t, props, new(Properties))
}

func TestDecodePropertiesBadKeyByte(t *testing.T) {
	b := bytes.NewBuffer([]byte{64, 1})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.Error(t, err)
	require.ErrorIs(t, err, ErrMalformedOffsetByteOutOfRange)
}

func TestDecodePropertiesInvalidForPacket(t *testing.T) {
	b := bytes.NewBuffer([]byte{1, 99})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.Error(t, err)
	require.ErrorIs(t, err, ErrProtocolViolationUnsupportedProperty)
}

func TestDecodePropertiesGeneralFailure(t *testing.T) {
	b := bytes.NewBuffer([]byte{10, 11, 202, 212, 19})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.Error(t, err)
}

func TestDecodePropertiesBadSubscriptionID(t *testing.T) {
	b := bytes.NewBuffer([]byte{10, 11, 255, 255, 255, 255, 255, 255, 255, 255})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.Error(t, err)
}

func TestDecodePropertiesBadUserProps(t *testing.T) {
	b := bytes.NewBuffer([]byte{10, 38, 255, 255, 255, 255, 255, 255, 255, 255})
	props := new(Properties)
	_, err := props.Decode(Reserved, b)
	require.Error(t, err)
}

func TestCopyProperties(t *testing.T) {
	require.EqualValues(t, propertiesStruct, propertiesStruct.Copy(true))
}

func TestCopyPropertiesNoTransfer(t *testing.T) {
	pkA := propertiesStruct
	pkB := pkA.Copy(false)

	// Properties which should never be transferred from one connection to another
	require.Equal(t, uint16(0), pkB.TopicAlias)
}
