package natpmp

import (
	"context"
	"net/netip"
	"testing"
	"time"

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

func Test_Client_rpc(t *testing.T) {
	t.Parallel()

	testCases := map[string]struct {
		ctx                       context.Context
		gateway                   netip.Addr
		request                   []byte
		responseSize              uint
		initialConnectionDuration time.Duration
		exchanges                 []udpExchange
		expectedResponse          []byte
		err                       error
		errMessage                string
	}{
		"gateway_ip_unspecified": {
			gateway:    netip.IPv6Unspecified(),
			request:    []byte{0, 0},
			err:        ErrGatewayIPUnspecified,
			errMessage: "gateway IP is unspecified",
		},
		"request_too_small": {
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0},
			initialConnectionDuration: time.Nanosecond, // doesn't matter
			err:                       ErrRequestSizeTooSmall,
			errMessage: `checking request: message size is too small: ` +
				`need at least 2 bytes and got 1 byte\(s\)`,
		},
		"write_error": {
			ctx:     context.Background(),
			gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request: []byte{0, 0},
			errMessage: `writing to connection: write udp ` +
				`127.0.0.1:[1-9][0-9]{0,4}->127.0.0.1:[1-9][0-9]{0,4}: ` +
				`i/o timeout`,
		},
		"call_error": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0, 1},
			initialConnectionDuration: time.Millisecond,
			exchanges: []udpExchange{
				{request: []byte{0, 1}, close: true},
			},
			err:        ErrConnectionTimeout,
			errMessage: "connection timeout: after 1ms",
		},
		"response_too_small": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0, 0},
			initialConnectionDuration: initialConnectionDuration,
			exchanges: []udpExchange{{
				request:  []byte{0, 0},
				response: []byte{1},
			}},
			err: ErrResponseSizeTooSmall,
			errMessage: `checking response: response size is too small: ` +
				`need at least 4 bytes and got 1 byte\(s\)`,
		},
		"unexpected_response_size": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
			responseSize:              5,
			initialConnectionDuration: initialConnectionDuration,
			exchanges: []udpExchange{{
				request:  []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
				response: []byte{0, 1, 2, 3}, // size 4
			}},
			err: ErrResponseSizeUnexpected,
			errMessage: `checking response: response size is unexpected: ` +
				`expected 5 bytes and got 4 byte\(s\)`,
		},
		"unknown_protocol_version": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
			responseSize:              16,
			initialConnectionDuration: initialConnectionDuration,
			exchanges: []udpExchange{{
				request:  []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
				response: []byte{0x1, 0x82, 0x0, 0x0, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
			}},
			err:        ErrProtocolVersionUnknown,
			errMessage: "checking response: protocol version is unknown: 1",
		},
		"unexpected_operation_code": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
			responseSize:              16,
			initialConnectionDuration: initialConnectionDuration,
			exchanges: []udpExchange{{
				request:  []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
				response: []byte{0x0, 0x88, 0x0, 0x0, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
			}},
			err:        ErrOperationCodeUnexpected,
			errMessage: "checking response: operation code is unexpected: expected 0x82 and got 0x88",
		},
		"failure_result_code": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
			responseSize:              16,
			initialConnectionDuration: initialConnectionDuration,
			exchanges: []udpExchange{{
				request:  []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
				response: []byte{0x0, 0x82, 0x0, 0x11, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
			}},
			err:        ErrResultCodeUnknown,
			errMessage: "checking response: result code: result code is unknown: 17",
		},
		"success": {
			ctx:                       context.Background(),
			gateway:                   netip.AddrFrom4([4]byte{127, 0, 0, 1}),
			request:                   []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
			responseSize:              16,
			initialConnectionDuration: initialConnectionDuration,
			exchanges: []udpExchange{{
				request:  []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
				response: []byte{0x0, 0x82, 0x0, 0x0, 0x0, 0x0, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
			}},
			expectedResponse: []byte{0x0, 0x82, 0x0, 0x0, 0x0, 0x0, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
		},
	}

	for name, testCase := range testCases {
		testCase := testCase
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			remoteAddress := launchUDPServer(t, testCase.exchanges)

			client := Client{
				serverPort:                uint16(remoteAddress.Port),
				initialConnectionDuration: testCase.initialConnectionDuration,
				maxRetries:                1,
			}

			response, err := client.rpc(testCase.ctx, testCase.gateway,
				testCase.request, testCase.responseSize)

			if testCase.errMessage != "" {
				if testCase.err != nil {
					assert.ErrorIs(t, err, testCase.err)
				}
				assert.Regexp(t, "^"+testCase.errMessage+"$", err.Error())
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, testCase.expectedResponse, response)
		})
	}
}
