package cert_test

import (
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/base64"
	"math/big"
	"net"
	"net/url"
	"testing"
	"time"

	"github.com/lestrrat-go/jwx/v3/cert"
	"github.com/lestrrat-go/jwx/v3/internal/jwxtest"
	"github.com/stretchr/testify/require"
)

func parseCIDR(s string) *net.IPNet {
	_, net, err := net.ParseCIDR(s)
	if err != nil {
		panic(err)
	}
	return net
}

func parseURI(s string) *url.URL {
	uri, err := url.Parse(s)
	if err != nil {
		panic(err)
	}
	return uri
}

func TestCert(t *testing.T) {
	restoreCertSettings()
	defer restoreCertSettings()

	privkey, err := jwxtest.GenerateRsaKey()
	require.NoError(t, err, `jwxtest.GenerateRsaKey`)

	testExtKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
	testUnknownExtKeyUsage := []asn1.ObjectIdentifier{[]int{1, 2, 3}, []int{2, 59, 1}}
	extraExtensionData := []byte("extra extension")
	oidExtensionSubjectKeyID := []int{2, 5, 29, 14}

	commonName := "test.example.com"
	template := x509.Certificate{
		SerialNumber: big.NewInt(1), // SerialNumbers must be non-negative since go1.19
		Subject: pkix.Name{
			CommonName:   commonName,
			Organization: []string{"Σ Acme Co"},
			Country:      []string{"US"},
			ExtraNames: []pkix.AttributeTypeAndValue{
				{
					Type:  []int{2, 5, 4, 42},
					Value: "Gopher",
				},
				// This should override the Country, above.
				{
					Type:  []int{2, 5, 4, 6},
					Value: "NL",
				},
			},
		},
		NotBefore: time.Unix(1000, 0),
		NotAfter:  time.Unix(100000, 0),

		SignatureAlgorithm: x509.SHA384WithRSA,

		SubjectKeyId: []byte{1, 2, 3, 4},
		KeyUsage:     x509.KeyUsageCertSign,

		ExtKeyUsage:        testExtKeyUsage,
		UnknownExtKeyUsage: testUnknownExtKeyUsage,

		BasicConstraintsValid: true,
		IsCA:                  true,

		OCSPServer:            []string{"http://ocsp.example.com"},
		IssuingCertificateURL: []string{"http://crt.example.com/ca1.crt"},

		DNSNames:       []string{"test.example.com"},
		EmailAddresses: []string{"gopher@golang.org"},
		IPAddresses:    []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")},
		URIs:           []*url.URL{parseURI("https://foo.com/wibble#foo")},

		PolicyIdentifiers:       []asn1.ObjectIdentifier{[]int{1, 2, 3}},
		PermittedDNSDomains:     []string{".example.com", "example.com"},
		ExcludedDNSDomains:      []string{"bar.example.com"},
		PermittedIPRanges:       []*net.IPNet{parseCIDR("192.168.1.1/16"), parseCIDR("1.2.3.4/8")},
		ExcludedIPRanges:        []*net.IPNet{parseCIDR("2001:db8::/48")},
		PermittedEmailAddresses: []string{"foo@example.com"},
		ExcludedEmailAddresses:  []string{".example.com", "example.com"},
		PermittedURIDomains:     []string{".bar.com", "bar.com"},
		ExcludedURIDomains:      []string{".bar2.com", "bar2.com"},

		CRLDistributionPoints: []string{"http://crl1.example.com/ca1.crl", "http://crl2.example.com/ca1.crl"},

		ExtraExtensions: []pkix.Extension{
			{
				Id:    []int{1, 2, 3, 4},
				Value: extraExtensionData,
			},
			// This extension should override the SubjectKeyId, above.
			{
				Id:       oidExtensionSubjectKeyID,
				Critical: false,
				Value:    []byte{0x04, 0x04, 4, 3, 2, 1},
			},
		},
	}

	b64, err := cert.Create(rand.Reader, &template, &template, &privkey.PublicKey, privkey)
	require.NoError(t, err, `cert.Certificate should succeed`)

	_, err = cert.Parse(b64)
	require.NoError(t, err, `cert.Parse should succeed`)
}

func TestParseRejectsInvalidCertificate(t *testing.T) {
	restoreCertSettings()
	defer restoreCertSettings()

	src := base64.StdEncoding.EncodeToString([]byte(`not a certificate`))
	_, err := cert.Parse([]byte(src))
	require.Error(t, err, `cert.Parse should reject base64 that is not an X.509 certificate`)
}

func TestParseRejectsDefaultMaxCertificateSize(t *testing.T) {
	restoreCertSettings()
	defer restoreCertSettings()

	oversized := make([]byte, testDefaultMaxCertificateSize+1)
	src := base64.StdEncoding.EncodeToString(oversized)

	_, err := cert.Parse([]byte(src))
	require.Error(t, err, `cert.Parse should enforce the default max certificate size`)
}

func TestParseAllowsUnlimitedCertificateSize(t *testing.T) {
	restoreCertSettings()
	defer restoreCertSettings()

	cert.Settings(cert.WithMaxCertificateSize(1))
	_, err := cert.Parse(certBytes)
	require.Error(t, err, `cert.Parse should respect the configured size limit`)

	cert.Settings(cert.WithMaxCertificateSize(0))
	_, err = cert.Parse(certBytes)
	require.NoError(t, err, `cert.Parse should succeed when the size limit is disabled`)
}

func TestSettingsRejectNegativeValues(t *testing.T) {
	restoreCertSettings()
	defer restoreCertSettings()

	t.Run(`WithMaxChainLength`, func(t *testing.T) {
		require.Panics(t, func() {
			cert.Settings(cert.WithMaxChainLength(-1))
		}, `negative max chain length should panic`)
	})

	t.Run(`WithMaxCertificateSize`, func(t *testing.T) {
		require.Panics(t, func() {
			cert.Settings(cert.WithMaxCertificateSize(-1))
		}, `negative max certificate size should panic`)
	})
}
