// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package client

import (
	"crypto"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"errors"
	"math/big"
	"net/http"
	"net/http/httptest"
	"net/url"
	goruntime "runtime"
	"testing"
	"time"

	"github.com/go-openapi/runtime"
	"github.com/go-openapi/strfmt"
	"github.com/go-openapi/testify/v2/assert"
	"github.com/go-openapi/testify/v2/require"
)

func TestRuntimeTLSOptions(t *testing.T) {
	fixtures := newTLSFixtures(t)

	t.Run("with TLSAuthConfig configured with files", func(t *testing.T) {
		opts := TLSClientOptions{
			CA:          fixtures.RSA.CAFile,
			Key:         fixtures.RSA.KeyFile,
			Certificate: fixtures.RSA.CertFile,
			ServerName:  fixtures.Subject,
		}

		cfg, err := TLSClientAuth(opts)
		require.NoError(t, err)

		require.NotNil(t, cfg)
		assert.Len(t, cfg.Certificates, 1)
		assert.NotNil(t, cfg.RootCAs)
		assert.EqualT(t, fixtures.Subject, cfg.ServerName)
	})

	t.Run("with loaded TLS material", func(t *testing.T) {
		t.Run("with TLSConfig from loaded RSA key/cert pair", func(t *testing.T) {
			opts := TLSClientOptions{
				LoadedKey:         fixtures.RSA.LoadedKey,
				LoadedCertificate: fixtures.RSA.LoadedCert,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			assert.Len(t, cfg.Certificates, 1)
		})

		t.Run("with TLSAuthConfig configured with loaded TLS Elliptic Curve key/certificate", func(t *testing.T) {
			opts := TLSClientOptions{
				LoadedKey:         fixtures.ECDSA.LoadedKey,
				LoadedCertificate: fixtures.ECDSA.LoadedCert,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			assert.Len(t, cfg.Certificates, 1)
		})

		t.Run("with TLSAuthConfig configured with loaded Ed25519 key/certificate", func(t *testing.T) {
			edKey, edCert := newEd25519KeyCert(t)
			opts := TLSClientOptions{
				LoadedKey:         edKey,
				LoadedCertificate: edCert,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			assert.Len(t, cfg.Certificates, 1)
		})

		t.Run("with TLSAuthConfig configured with loaded Certificate Authority", func(t *testing.T) {
			opts := TLSClientOptions{
				LoadedCA: fixtures.RSA.LoadedCA,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			assert.NotNil(t, cfg.RootCAs)
		})

		t.Run("with TLSAuthConfig configured with loaded CA pool", func(t *testing.T) {
			pool := x509.NewCertPool()
			pool.AddCert(fixtures.RSA.LoadedCA)

			opts := TLSClientOptions{
				LoadedCAPool: pool,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			require.NotNil(t, cfg.RootCAs)
			require.Equal(t, pool, cfg.RootCAs)
		})

		t.Run("with TLSAuthConfig configured with loaded CA and CA pool", func(t *testing.T) {
			pool := systemCAPool(t)
			opts := TLSClientOptions{
				LoadedCAPool: pool,
				LoadedCA:     fixtures.RSA.LoadedCA,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			require.NotNil(t, cfg.RootCAs)

			// verify that the CA cert is indeed valid against the configured pool.
			// NOTE: fixtures may be expired certs, but may validate with a fixed date in the past.
			chains, err := fixtures.RSA.LoadedCA.Verify(x509.VerifyOptions{
				Roots:       cfg.RootCAs,
				CurrentTime: time.Date(2027, 1, 1, 1, 1, 1, 1, time.UTC),
			})
			require.NoError(t, err)
			require.NotEmpty(t, chains)
		})

		t.Run("with TLSAuthConfig with VerifyPeer option", func(t *testing.T) {
			verify := func(_ [][]byte, _ [][]*x509.Certificate) error {
				return nil
			}

			opts := TLSClientOptions{
				InsecureSkipVerify:    true,
				VerifyPeerCertificate: verify,
			}

			cfg, err := TLSClientAuth(opts)
			require.NoError(t, err)
			require.NotNil(t, cfg)
			assert.TrueT(t, cfg.InsecureSkipVerify)
			assert.NotNil(t, cfg.VerifyPeerCertificate)
		})
	})
}

func TestRuntimeManualCertificateValidation(t *testing.T) {
	// test manual verification of server certificates
	// against root certificate on client side.
	//
	// The client compares the received cert against the root cert,
	// explicitly omitting DNSName check.
	fixtures := newTLSFixtures(t)
	result := []task{
		{false, taskOneContent, 1},
		{false, taskTwoContent, 2},
	}
	host, clean := testTLSServer(t, fixtures, result)
	t.Cleanup(clean)
	var certVerifyCalled bool
	client := testTLSClient(t, fixtures, &certVerifyCalled)
	rt := NewWithClient(host, "/", []string{schemeHTTPS}, client)

	var received []task
	operation := &runtime.ClientOperation{
		ID:          operationID,
		Method:      http.MethodGet,
		PathPattern: "/",
		Params: runtime.ClientRequestWriterFunc(func(_ runtime.ClientRequest, _ strfmt.Registry) error {
			return nil
		}),
		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
			if response.Code() == http.StatusOK {
				if e := consumer.Consume(response.Body(), &received); e != nil {
					return nil, e
				}
				return result, nil
			}
			return nil, errors.New("generic error")
		}),
	}

	resp, err := rt.Submit(operation)
	require.NoError(t, err)

	require.NotEmpty(t, resp)
	assert.IsType(t, []task{}, resp)

	assert.TrueTf(t, certVerifyCalled, "the client cert verification has not been called")
	assert.Equal(t, result, received)
}

func testTLSServer(t testing.TB, fixtures *tlsFixtures, expectedResult []task) (string, func()) {
	server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
		rw.Header().Add(runtime.HeaderContentType, runtime.JSONMime)
		rw.WriteHeader(http.StatusOK)
		jsongen := json.NewEncoder(rw)
		assert.NoError(t, jsongen.Encode(expectedResult))
	}))

	// create server tls config
	serverCACertPool := x509.NewCertPool()
	serverCACertPool.AddCert(fixtures.Server.LoadedCA)
	// load server certs
	serverCert, err := tls.LoadX509KeyPair(
		fixtures.Server.CertFile,
		fixtures.Server.KeyFile,
	)
	require.NoError(t, err)

	server.TLS = &tls.Config{
		RootCAs:      serverCACertPool,
		MinVersion:   tls.VersionTLS12,
		Certificates: []tls.Certificate{serverCert},
	}
	require.NoError(t, err)

	server.StartTLS()
	testURL, err := url.Parse(server.URL)
	require.NoError(t, err)

	return testURL.Host, server.Close
}

func testTLSClient(t testing.TB, fixtures *tlsFixtures, verifyCalled *bool) *http.Client {
	client, err := TLSClient(TLSClientOptions{
		InsecureSkipVerify: true,
		VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
			*verifyCalled = true

			caCertPool := x509.NewCertPool()
			caCertPool.AddCert(fixtures.RSA.LoadedCA)

			opts := x509.VerifyOptions{
				Roots:       caCertPool,
				CurrentTime: time.Date(2027, time.July, 1, 1, 1, 1, 1, time.UTC),
			}

			cert, e := x509.ParseCertificate(rawCerts[0])
			if e != nil {
				return e
			}

			_, e = cert.Verify(opts)
			return e
		},
	})
	require.NoError(t, err)

	return client
}

type (
	tlsFixtures struct {
		RSA     tlsFixture
		ECDSA   tlsFixture
		Server  tlsFixture
		Subject string
	}

	tlsFixture struct {
		LoadedCA   *x509.Certificate
		LoadedCert *x509.Certificate
		LoadedKey  crypto.PrivateKey

		CAFile   string
		KeyFile  string
		CertFile string
	}
)

func newEd25519KeyCert(t testing.TB) (ed25519.PrivateKey, *x509.Certificate) {
	t.Helper()

	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)

	template := &x509.Certificate{
		SerialNumber: big.NewInt(42),
		Subject:      pkix.Name{CommonName: "ed25519-test"},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
	}

	der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
	require.NoError(t, err)
	cert, err := x509.ParseCertificate(der)
	require.NoError(t, err)

	return priv, cert
}

func systemCAPool(t testing.TB) *x509.CertPool {
	if goruntime.GOOS == "windows" {
		// Windows doesn't have the system cert pool.
		return x509.NewCertPool()
	}

	pool, err := x509.SystemCertPool()
	require.NoError(t, err)

	return pool
}
