package qlog

import (
	"bytes"
	"encoding/json"
	"testing"
	"time"

	"github.com/francoispqt/gojay"
	"github.com/quic-go/quic-go/internal/protocol"
	"github.com/quic-go/quic-go/internal/qerr"
	"github.com/quic-go/quic-go/logging"
	"github.com/stretchr/testify/require"
)

func check(t *testing.T, f logging.Frame, expected map[string]interface{}) {
	buf := &bytes.Buffer{}
	enc := gojay.NewEncoder(buf)
	err := enc.Encode(frame{Frame: f})
	require.NoError(t, err)
	data := buf.Bytes()
	require.True(t, json.Valid(data))
	checkEncoding(t, data, expected)
}

func TestPingFrame(t *testing.T) {
	check(t,
		&logging.PingFrame{},
		map[string]interface{}{
			"frame_type": "ping",
		},
	)
}

func TestAckFrame(t *testing.T) {
	tests := []struct {
		name     string
		frame    *logging.AckFrame
		expected map[string]interface{}
	}{
		{
			name: "with delay and single packet range",
			frame: &logging.AckFrame{
				DelayTime: 86 * time.Millisecond,
				AckRanges: []logging.AckRange{{Smallest: 120, Largest: 120}},
			},
			expected: map[string]interface{}{
				"frame_type":   "ack",
				"ack_delay":    86,
				"acked_ranges": [][]float64{{120}},
			},
		},
		{
			name: "without delay",
			frame: &logging.AckFrame{
				AckRanges: []logging.AckRange{{Smallest: 120, Largest: 120}},
			},
			expected: map[string]interface{}{
				"frame_type":   "ack",
				"acked_ranges": [][]float64{{120}},
			},
		},
		{
			name: "with ECN counts",
			frame: &logging.AckFrame{
				AckRanges: []logging.AckRange{{Smallest: 120, Largest: 120}},
				ECT0:      10,
				ECT1:      100,
				ECNCE:     1000,
			},
			expected: map[string]interface{}{
				"frame_type":   "ack",
				"acked_ranges": [][]float64{{120}},
				"ect0":         10,
				"ect1":         100,
				"ce":           1000,
			},
		},
		{
			name: "with multiple ranges",
			frame: &logging.AckFrame{
				DelayTime: 86 * time.Millisecond,
				AckRanges: []logging.AckRange{
					{Smallest: 5, Largest: 50},
					{Smallest: 100, Largest: 120},
				},
			},
			expected: map[string]interface{}{
				"frame_type": "ack",
				"ack_delay":  86,
				"acked_ranges": [][]float64{
					{5, 50},
					{100, 120},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			check(t, tt.frame, tt.expected)
		})
	}
}

func TestResetStreamFrame(t *testing.T) {
	check(t,
		&logging.ResetStreamFrame{
			StreamID:  987,
			FinalSize: 1234,
			ErrorCode: 42,
		},
		map[string]interface{}{
			"frame_type": "reset_stream",
			"stream_id":  987,
			"error_code": 42,
			"final_size": 1234,
		},
	)
}

func TestStopSendingFrame(t *testing.T) {
	check(t,
		&logging.StopSendingFrame{
			StreamID:  987,
			ErrorCode: 42,
		},
		map[string]interface{}{
			"frame_type": "stop_sending",
			"stream_id":  987,
			"error_code": 42,
		},
	)
}

func TestCryptoFrame(t *testing.T) {
	check(t,
		&logging.CryptoFrame{
			Offset: 1337,
			Length: 6,
		},
		map[string]interface{}{
			"frame_type": "crypto",
			"offset":     1337,
			"length":     6,
		},
	)
}

func TestNewTokenFrame(t *testing.T) {
	check(t,
		&logging.NewTokenFrame{
			Token: []byte{0xde, 0xad, 0xbe, 0xef},
		},
		map[string]interface{}{
			"frame_type": "new_token",
			"token":      map[string]interface{}{"data": "deadbeef"},
		},
	)
}

func TestStreamFrame(t *testing.T) {
	tests := []struct {
		name     string
		frame    *logging.StreamFrame
		expected map[string]interface{}
	}{
		{
			name: "with FIN",
			frame: &logging.StreamFrame{
				StreamID: 42,
				Offset:   1337,
				Fin:      true,
				Length:   9876,
			},
			expected: map[string]interface{}{
				"frame_type": "stream",
				"stream_id":  42,
				"offset":     1337,
				"fin":        true,
				"length":     9876,
			},
		},
		{
			name: "without FIN",
			frame: &logging.StreamFrame{
				StreamID: 42,
				Offset:   1337,
				Length:   3,
			},
			expected: map[string]interface{}{
				"frame_type": "stream",
				"stream_id":  42,
				"offset":     1337,
				"length":     3,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			check(t, tt.frame, tt.expected)
		})
	}
}

func TestMaxDataFrame(t *testing.T) {
	check(t,
		&logging.MaxDataFrame{
			MaximumData: 1337,
		},
		map[string]interface{}{
			"frame_type": "max_data",
			"maximum":    1337,
		},
	)
}

func TestMaxStreamDataFrame(t *testing.T) {
	check(t,
		&logging.MaxStreamDataFrame{
			StreamID:          1234,
			MaximumStreamData: 1337,
		},
		map[string]interface{}{
			"frame_type": "max_stream_data",
			"stream_id":  1234,
			"maximum":    1337,
		},
	)
}

func TestMaxStreamsFrame(t *testing.T) {
	check(t,
		&logging.MaxStreamsFrame{
			Type:         protocol.StreamTypeBidi,
			MaxStreamNum: 42,
		},
		map[string]interface{}{
			"frame_type":  "max_streams",
			"stream_type": "bidirectional",
			"maximum":     42,
		},
	)
}

func TestDataBlockedFrame(t *testing.T) {
	check(t,
		&logging.DataBlockedFrame{
			MaximumData: 1337,
		},
		map[string]interface{}{
			"frame_type": "data_blocked",
			"limit":      1337,
		},
	)
}

func TestStreamDataBlockedFrame(t *testing.T) {
	check(t,
		&logging.StreamDataBlockedFrame{
			StreamID:          42,
			MaximumStreamData: 1337,
		},
		map[string]interface{}{
			"frame_type": "stream_data_blocked",
			"stream_id":  42,
			"limit":      1337,
		},
	)
}

func TestStreamsBlockedFrame(t *testing.T) {
	check(t,
		&logging.StreamsBlockedFrame{
			Type:        protocol.StreamTypeUni,
			StreamLimit: 123,
		},
		map[string]interface{}{
			"frame_type":  "streams_blocked",
			"stream_type": "unidirectional",
			"limit":       123,
		},
	)
}

func TestNewConnectionIDFrame(t *testing.T) {
	check(t,
		&logging.NewConnectionIDFrame{
			SequenceNumber:      42,
			RetirePriorTo:       24,
			ConnectionID:        protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
			StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf},
		},
		map[string]interface{}{
			"frame_type":            "new_connection_id",
			"sequence_number":       42,
			"retire_prior_to":       24,
			"length":                4,
			"connection_id":         "deadbeef",
			"stateless_reset_token": "000102030405060708090a0b0c0d0e0f",
		},
	)
}

func TestRetireConnectionIDFrame(t *testing.T) {
	check(t,
		&logging.RetireConnectionIDFrame{
			SequenceNumber: 1337,
		},
		map[string]interface{}{
			"frame_type":      "retire_connection_id",
			"sequence_number": 1337,
		},
	)
}

func TestPathChallengeFrame(t *testing.T) {
	check(t,
		&logging.PathChallengeFrame{
			Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01},
		},
		map[string]interface{}{
			"frame_type": "path_challenge",
			"data":       "deadbeefcafec001",
		},
	)
}

func TestPathResponseFrame(t *testing.T) {
	check(t,
		&logging.PathResponseFrame{
			Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01},
		},
		map[string]interface{}{
			"frame_type": "path_response",
			"data":       "deadbeefcafec001",
		},
	)
}

func TestConnectionCloseFrame(t *testing.T) {
	tests := []struct {
		name     string
		frame    *logging.ConnectionCloseFrame
		expected map[string]interface{}
	}{
		{
			name: "application error code",
			frame: &logging.ConnectionCloseFrame{
				IsApplicationError: true,
				ErrorCode:          1337,
				ReasonPhrase:       "lorem ipsum",
			},
			expected: map[string]interface{}{
				"frame_type":     "connection_close",
				"error_space":    "application",
				"error_code":     1337,
				"raw_error_code": 1337,
				"reason":         "lorem ipsum",
			},
		},
		{
			name: "transport error code",
			frame: &logging.ConnectionCloseFrame{
				ErrorCode:    uint64(qerr.FlowControlError),
				ReasonPhrase: "lorem ipsum",
			},
			expected: map[string]interface{}{
				"frame_type":     "connection_close",
				"error_space":    "transport",
				"error_code":     "flow_control_error",
				"raw_error_code": int(qerr.FlowControlError),
				"reason":         "lorem ipsum",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			check(t, tt.frame, tt.expected)
		})
	}
}

func TestHandshakeDoneFrame(t *testing.T) {
	check(t,
		&logging.HandshakeDoneFrame{},
		map[string]interface{}{
			"frame_type": "handshake_done",
		},
	)
}

func TestDatagramFrame(t *testing.T) {
	check(t,
		&logging.DatagramFrame{Length: 1337},
		map[string]interface{}{
			"frame_type": "datagram",
			"length":     1337,
		},
	)
}
