// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package pool

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io"
	"net"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/tlsutil"
)

func TestPeekForTLS_not_TLS(t *testing.T) {
	type testcase struct {
		name     string
		connData []byte
	}

	var cases []testcase
	for _, rpcType := range []RPCType{
		RPCConsul,
		RPCRaft,
		RPCMultiplex,
		RPCTLS,
		RPCMultiplexV2,
		RPCSnapshot,
		RPCGossip,
		RPCTLSInsecure,
		RPCGRPC,
	} {
		cases = append(cases, testcase{
			name:     fmt.Sprintf("tcp rpc type byte %d", rpcType),
			connData: []byte{byte(rpcType), 'h', 'e', 'l', 'l', 'o'},
		})
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			dead := time.Now().Add(1 * time.Second)
			serverConn, clientConn, err := deadlineNetPipe(dead)
			require.NoError(t, err)
			go func() {
				_, _ = clientConn.Write(tc.connData)
				_ = clientConn.Close()
			}()
			defer serverConn.Close()

			wrapped, isTLS, err := PeekForTLS(serverConn)
			require.NoError(t, err)
			require.False(t, isTLS)

			all, err := io.ReadAll(wrapped)
			require.NoError(t, err)
			require.Equal(t, tc.connData, all)
		})
	}
}

func TestPeekForTLS_actual_TLS(t *testing.T) {
	type testcase struct {
		name     string
		connData []byte
	}

	var cases []testcase
	for _, rpcType := range []RPCType{
		RPCConsul,
		RPCRaft,
		RPCMultiplex,
		RPCTLS,
		RPCMultiplexV2,
		RPCSnapshot,
		RPCGossip,
		RPCTLSInsecure,
		RPCGRPC,
	} {
		cases = append(cases, testcase{
			name:     fmt.Sprintf("tcp rpc type byte %d", rpcType),
			connData: []byte{byte(rpcType), 'h', 'e', 'l', 'l', 'o'},
		})
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			testPeekForTLS_withTLS(t, tc.connData)
		})
	}
}

func testPeekForTLS_withTLS(t *testing.T, connData []byte) {
	t.Helper()

	cert, caPEM, err := generateTestCert("server.dc1.consul")
	require.NoError(t, err)

	roots := x509.NewCertPool()
	require.True(t, roots.AppendCertsFromPEM(caPEM))

	dead := time.Now().Add(1 * time.Second)
	serverConn, clientConn, err := deadlineNetPipe(dead)
	require.NoError(t, err)

	var (
		clientErrCh      = make(chan error, 1)
		serverErrCh      = make(chan error, 1)
		serverGotPayload []byte
	)
	go func(conn net.Conn) { // Client
		config := &tls.Config{
			MinVersion: tls.VersionTLS12,
			RootCAs:    roots,
			ServerName: "server.dc1.consul",
			NextProtos: []string{"foo/bar"},
		}

		tlsConn := tls.Client(conn, config)
		defer tlsConn.Close()

		if err := tlsConn.Handshake(); err != nil {
			clientErrCh <- err
			return
		}

		_, err = tlsConn.Write(connData)
		clientErrCh <- err
	}(clientConn)

	go func(conn net.Conn) { // Server
		defer conn.Close()

		wrapped, isTLS, err := PeekForTLS(conn)
		if err != nil {
			serverErrCh <- err
			return
		} else if !isTLS {
			serverErrCh <- errors.New("expected to have peeked TLS but did not")
			return
		}

		config := &tls.Config{
			MinVersion:   tls.VersionTLS12,
			RootCAs:      roots,
			Certificates: []tls.Certificate{cert},
			ServerName:   "server.dc1.consul",
			NextProtos:   []string{"foo/bar"},
		}

		tlsConn := tls.Server(wrapped, config)
		defer tlsConn.Close()

		if err := tlsConn.Handshake(); err != nil {
			serverErrCh <- err
			return
		}

		all, err := io.ReadAll(tlsConn)
		if err != nil {
			serverErrCh <- err
			return
		}

		serverGotPayload = all
		serverErrCh <- nil
	}(serverConn)

	require.NoError(t, <-clientErrCh)
	require.NoError(t, <-serverErrCh)

	require.Equal(t, connData, serverGotPayload)
}

func deadlineNetPipe(deadline time.Time) (net.Conn, net.Conn, error) {
	server, client := net.Pipe()

	if err := server.SetDeadline(deadline); err != nil {
		server.Close()
		client.Close()
		return nil, nil, err
	}
	if err := client.SetDeadline(deadline); err != nil {
		server.Close()
		client.Close()
		return nil, nil, err
	}

	return server, client, nil
}

func generateTestCert(serverName string) (cert tls.Certificate, caPEM []byte, err error) {
	signer, _, err := tlsutil.GeneratePrivateKey()
	if err != nil {
		return tls.Certificate{}, nil, err
	}

	ca, _, err := tlsutil.GenerateCA(tlsutil.CAOpts{Signer: signer})
	if err != nil {
		return tls.Certificate{}, nil, err
	}

	certificate, privateKey, err := tlsutil.GenerateCert(tlsutil.CertOpts{
		Signer:      signer,
		CA:          ca,
		Name:        "Test Cert Name",
		Days:        365,
		DNSNames:    []string{serverName},
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	})
	if err != nil {
		return tls.Certificate{}, nil, err
	}

	cert, err = tls.X509KeyPair([]byte(certificate), []byte(privateKey))
	if err != nil {
		return tls.Certificate{}, nil, err
	}

	return cert, []byte(ca), nil
}
