package netxlite

import (
	"context"
	"crypto/tls"
	"errors"
	"net"
	"sync"
	"testing"
	"time"

	"github.com/apex/log"
	"github.com/ooni/probe-cli/v3/internal/model/mocks"
	utls "gitlab.com/yawning/utls.git"
)

func TestNewTLSHandshakerUTLS(t *testing.T) {
	th := NewTLSHandshakerUTLS(log.Log, &utls.HelloChrome_83)
	logger := th.(*tlsHandshakerLogger)
	if logger.DebugLogger != log.Log {
		t.Fatal("invalid logger")
	}
	configurable := logger.TLSHandshaker.(*tlsHandshakerConfigurable)
	if configurable.NewConn == nil {
		t.Fatal("expected non-nil NewConn")
	}
}

func TestUTLSConn(t *testing.T) {
	t.Run("Handshake", func(t *testing.T) {
		t.Run("not interrupted with success", func(t *testing.T) {
			ctx := context.Background()
			conn := &utlsConn{
				testableHandshake: func() error {
					return nil
				},
			}
			err := conn.HandshakeContext(ctx)
			if err != nil {
				t.Fatal(err)
			}
		})

		t.Run("not interrupted with failure", func(t *testing.T) {
			expected := errors.New("mocked error")
			ctx := context.Background()
			conn := &utlsConn{
				testableHandshake: func() error {
					return expected
				},
			}
			err := conn.HandshakeContext(ctx)
			if !errors.Is(err, expected) {
				t.Fatal("not the error we expected", err)
			}
		})

		t.Run("interrupted", func(t *testing.T) {
			wg := sync.WaitGroup{}
			wg.Add(1)
			sigch := make(chan interface{})
			ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
			defer cancel()
			conn := &utlsConn{
				testableHandshake: func() error {
					defer wg.Done()
					<-sigch
					return nil
				},
			}
			err := conn.HandshakeContext(ctx)
			if !errors.Is(err, context.DeadlineExceeded) {
				t.Fatal("not the error we expected", err)
			}
			close(sigch)
			wg.Wait()
		})

		t.Run("with panic", func(t *testing.T) {
			wg := sync.WaitGroup{}
			wg.Add(1)
			ctx := context.Background()
			conn := &utlsConn{
				testableHandshake: func() error {
					defer wg.Done()
					panic("mascetti")
				},
			}
			err := conn.HandshakeContext(ctx)
			if !errors.Is(err, ErrUTLSHandshakePanic) {
				t.Fatal("not the error we expected", err)
			}
			wg.Wait()
		})
	})

	t.Run("NetConn", func(t *testing.T) {
		factory := newConnUTLS(&utls.HelloChrome_70)
		conn := &mocks.Conn{}
		tconn, err := factory(conn, &tls.Config{})
		if err != nil {
			t.Fatal(err)
		}
		// TODO(https://github.com/ooni/probe/issues/2222): we cannot avoid
		// castedTconn until we use oocrypto >= v0.2 which uses go1.19. In turn,
		// we cannot use go1.19 as our main version until we upgrade psiphon
		// such that it builds using go1.19, which is the issue in #2222.
		castedTconn := tconn.(*utlsConn)
		if castedTconn.NetConn() != conn {
			t.Fatal("NetConn is not WAI")
		}
	})
}

func Test_newConnUTLSWithHelloID(t *testing.T) {
	tests := []struct {
		name        string
		config      *tls.Config
		cid         *utls.ClientHelloID
		wantNilConn bool
		wantErr     error
	}{{
		name: "with only supported fields",
		config: &tls.Config{
			DynamicRecordSizingDisabled: true,
			InsecureSkipVerify:          true,
			NextProtos:                  []string{"h3"},
			RootCAs:                     NewDefaultCertPool(),
			ServerName:                  "ooni.org",
		},
		cid:         &utls.HelloFirefox_55,
		wantNilConn: false,
		wantErr:     nil,
	}, {
		name: "with unsupported fields",
		config: &tls.Config{
			Time: func() time.Time {
				return time.Now()
			},
		},
		cid:         &utls.HelloChrome_58,
		wantNilConn: true,
		wantErr:     errUTLSIncompatibleStdlibConfig,
	}}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			conn, err := net.Dial("udp", "8.8.8.8:443") // we just need a conn
			if err != nil {
				t.Fatal(err)
			}
			defer conn.Close()
			got, err := newConnUTLSWithHelloID(conn, tt.config, tt.cid)
			if !errors.Is(err, tt.wantErr) {
				t.Fatal("unexpected err", err)
			}
			if got != nil && tt.wantNilConn {
				t.Fatal("expected nil conn here")
			}
		})
	}
}
