// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package e2e

import (
	"fmt"
	"math/rand"
	"testing"
	"time"

	"github.com/pion/dtls/v3"
	"github.com/pion/dtls/v3/pkg/crypto/selfsign"
	dtlsnet "github.com/pion/dtls/v3/pkg/net"
	transportTest "github.com/pion/transport/v4/test"
	"github.com/stretchr/testify/assert"
)

const (
	flightInterval   = time.Millisecond * 100
	lossyTestTimeout = 30 * time.Second
)

// DTLS Client/Server over a lossy transport, just asserts it can handle at increasing increments

func TestPionE2ELossy(t *testing.T) { //nolint:cyclop
	// Check for leaking routines
	report := transportTest.CheckRoutines(t)
	defer report()

	type runResult struct {
		dtlsConn *dtls.Conn
		err      error
	}

	serverCert, err := selfsign.GenerateSelfSigned()
	assert.NoError(t, err)

	clientCert, err := selfsign.GenerateSelfSigned()
	assert.NoError(t, err)

	for _, test := range []struct {
		LossChanceRange             int
		DoClientAuth                bool
		CipherSuites                []dtls.CipherSuiteID
		MTU                         int
		DisableServerFlightInterval bool
	}{
		{
			LossChanceRange: 0,
		},
		{
			LossChanceRange: 10,
		},
		{
			LossChanceRange: 20,
		},
		{
			LossChanceRange: 50,
		},
		{
			LossChanceRange: 0,
			DoClientAuth:    true,
		},
		{
			LossChanceRange: 10,
			DoClientAuth:    true,
		},
		{
			LossChanceRange: 20,
			DoClientAuth:    true,
		},
		{
			LossChanceRange: 50,
			DoClientAuth:    true,
		},
		{
			LossChanceRange: 0,
			CipherSuites:    []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
		},
		{
			LossChanceRange: 10,
			CipherSuites:    []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
		},
		{
			LossChanceRange: 20,
			CipherSuites:    []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
		},
		{
			LossChanceRange: 50,
			CipherSuites:    []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
		},
		{
			LossChanceRange: 10,
			MTU:             100,
			DoClientAuth:    true,
		},
		{
			LossChanceRange: 20,
			MTU:             100,
			DoClientAuth:    true,
		},
		{
			LossChanceRange: 50,
			MTU:             100,
			DoClientAuth:    true,
		},
		// Incoming retransmitted handshakes should cause us to retransmit. Disabling the FlightInterval on one side
		// means that a incoming re-transmissions causes the retransmission to be fired
		{
			LossChanceRange:             10,
			DisableServerFlightInterval: true,
		},
		{
			LossChanceRange:             20,
			DisableServerFlightInterval: true,
		},
		{
			LossChanceRange:             50,
			DisableServerFlightInterval: true,
		},
	} {
		name := fmt.Sprintf("Loss%d_MTU%d", test.LossChanceRange, test.MTU)
		if test.DoClientAuth {
			name += "_WithCliAuth"
		}
		for _, ciph := range test.CipherSuites {
			name += "_With" + ciph.String()
		}
		if test.DisableServerFlightInterval {
			name += "_WithNoServerFlightInterval"
		}

		test := test
		t.Run(name, func(t *testing.T) {
			// Limit runtime in case of deadlocks
			lim := transportTest.TimeOut(lossyTestTimeout + time.Second)
			defer lim.Stop()

			chosenLoss := rand.Intn(9) + test.LossChanceRange //nolint:gosec
			serverDone := make(chan runResult)
			clientDone := make(chan runResult)
			br := transportTest.NewBridge()

			assert.NoError(t, br.SetLossChance(chosenLoss))

			go func() {
				clientOpts := []dtls.ClientOption{
					dtls.WithFlightInterval(flightInterval),
					dtls.WithInsecureSkipVerify(true),
					dtls.WithDisableRetransmitBackoff(true),
				}
				if len(test.CipherSuites) > 0 {
					clientOpts = append(clientOpts, dtls.WithCipherSuites(test.CipherSuites...))
				}
				if test.MTU > 0 {
					clientOpts = append(clientOpts, dtls.WithMTU(test.MTU))
				}

				if test.DoClientAuth {
					clientOpts = append(clientOpts, dtls.WithCertificates(clientCert))
				}

				client, startupErr := dtls.ClientWithOptions(
					dtlsnet.PacketConnFromConn(br.GetConn0()),
					br.GetConn0().RemoteAddr(),
					clientOpts...,
				)
				clientDone <- runResult{client, startupErr}
			}()

			go func() {
				serverOpts := []dtls.ServerOption{
					dtls.WithCertificates(serverCert),
					dtls.WithFlightInterval(flightInterval),
					dtls.WithDisableRetransmitBackoff(true),
				}
				if test.MTU > 0 {
					serverOpts = append(serverOpts, dtls.WithMTU(test.MTU))
				}

				if test.DoClientAuth {
					serverOpts = append(serverOpts, dtls.WithClientAuth(dtls.RequireAnyClientCert))
				}

				if test.DisableServerFlightInterval {
					serverOpts = append(serverOpts, dtls.WithFlightInterval(time.Hour))
				}

				server, startupErr := dtls.ServerWithOptions(
					dtlsnet.PacketConnFromConn(br.GetConn1()),
					br.GetConn1().RemoteAddr(),
					serverOpts...,
				)
				serverDone <- runResult{server, startupErr}
			}()

			testTimer := time.NewTimer(lossyTestTimeout)
			var serverConn, clientConn *dtls.Conn
			defer func() {
				if serverConn != nil {
					assert.NoError(t, serverConn.Close())
				}
				if clientConn != nil {
					assert.NoError(t, clientConn.Close())
				}
			}()

			for serverConn == nil || clientConn == nil {
				br.Tick()
				select {
				case serverResult := <-serverDone:
					if serverResult.err != nil {
						assert.Failf(t, "Fail, serverError", "clientComplete(%t) serverComplete(%t) LossChance(%d) error(%v)",
							clientConn != nil, serverConn != nil, chosenLoss, serverResult.err)

						return
					}

					serverConn = serverResult.dtlsConn
				case clientResult := <-clientDone:
					if clientResult.err != nil {
						assert.Failf(t, "Fail, clientError", "clientComplete(%t) serverComplete(%t) LossChance(%d) error(%v)",
							clientConn != nil, serverConn != nil, chosenLoss, clientResult.err)

						return
					}

					clientConn = clientResult.dtlsConn
				case <-testTimer.C:
					assert.Failf(t, "Test expired", "clientComplete(%t) serverComplete(%t) LossChance(%d)",
						clientConn != nil, serverConn != nil, chosenLoss)

					return
				case <-time.After(10 * time.Millisecond):
				}
			}
		})
	}
}
