package api_test

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"errors"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"testing"
	"time"

	. "github.com/smartystreets/goconvey/convey"
	"gopkg.in/resty.v1"

	"zotregistry.dev/zot/v2/pkg/api"
	"zotregistry.dev/zot/v2/pkg/api/config"
	test "zotregistry.dev/zot/v2/pkg/test/common"
	"zotregistry.dev/zot/v2/pkg/test/mocks"
	tlsutils "zotregistry.dev/zot/v2/pkg/test/tls"
)

var ErrUnexpectedError = errors.New("error: unexpected error")

// setupTestCerts generates CA, server, and client certificates for testing.
// Returns paths to certificate files and PEM data for CA cert.
func setupTestCerts(t *testing.T) (
	string, string, string, string, string, []byte,
) {
	t.Helper()
	tempDir := t.TempDir()

	// Generate CA certificate (10 years validity, matching gen_certs.sh)
	caOpts := &tlsutils.CertificateOptions{
		CommonName: "*",
		NotAfter:   time.Now().AddDate(10, 0, 0),
	}
	caCertPEM, caKeyPEM, err := tlsutils.GenerateCACert(caOpts)
	if err != nil {
		t.Fatalf("Failed to generate CA cert: %v", err)
	}

	caCertPath := path.Join(tempDir, "ca.crt")
	caKeyPath := path.Join(tempDir, "ca.key")
	err = os.WriteFile(caCertPath, caCertPEM, 0o600)
	if err != nil {
		t.Fatalf("Failed to write CA cert: %v", err)
	}
	_ = os.WriteFile(caKeyPath, caKeyPEM, 0o600)

	// Generate server certificate
	serverCertPath := path.Join(tempDir, "server.cert")
	serverKeyPath := path.Join(tempDir, "server.key")
	serverOpts := &tlsutils.CertificateOptions{
		Hostname:           "127.0.0.1",
		CommonName:         "*",
		OrganizationalUnit: "TestServer",
		NotAfter:           time.Now().AddDate(10, 0, 0),
	}
	err = tlsutils.GenerateServerCertToFile(caCertPEM, caKeyPEM, serverCertPath, serverKeyPath, serverOpts)
	if err != nil {
		t.Fatalf("Failed to generate server cert: %v", err)
	}

	// Generate client certificate (10 years validity, matching gen_certs.sh)
	clientCertPath := path.Join(tempDir, "client.cert")
	clientKeyPath := path.Join(tempDir, "client.key")
	clientOpts := &tlsutils.CertificateOptions{
		CommonName:         "testclient",
		OrganizationalUnit: "TestClient",
		NotAfter:           time.Now().AddDate(10, 0, 0),
	}
	err = tlsutils.GenerateClientCertToFile(caCertPEM, caKeyPEM, clientCertPath, clientKeyPath, clientOpts)
	if err != nil {
		t.Fatalf("Failed to generate client cert: %v", err)
	}

	return caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath, caCertPEM
}

// mTLSTestCase defines a test case for mTLS identity extraction.
type mTLSTestCase struct {
	name              string
	clientCertOptions *tlsutils.CertificateOptions
	mtlsConfig        *config.MTLSConfig
	allowedUsers      []string // Users allowed in access control
	expectedIdentity  string   // Expected identity extracted from cert
	expectedStatus    int      // Expected HTTP status code
	description       string   // Test description
}

// getExpectedSubjectDN constructs the expected Subject DN string based on the certificate options.
// This matches the format used by tlsutils for client certificates.
func getExpectedSubjectDN(commonName string) string {
	subject := pkix.Name{
		Organization:  []string{"Test Client"},
		Country:       []string{"US"},
		Province:      []string{""},
		Locality:      []string{"San Francisco"},
		StreetAddress: []string{""},
		PostalCode:    []string{""},
	}
	if commonName != "" {
		subject.CommonName = commonName
	}

	return subject.String()
}

// runMTLSTest executes a single mTLS test case.
func runMTLSTest(t *testing.T, testCase mTLSTestCase) {
	t.Helper()

	tempDir := t.TempDir()

	// Generate CA certificate
	caCert, caKey, err := tlsutils.GenerateCACert()
	So(err, ShouldBeNil)
	caCertPath := path.Join(tempDir, "ca.crt")
	err = os.WriteFile(caCertPath, caCert, 0o600)
	So(err, ShouldBeNil)

	// Generate server certificate
	serverCertPath := path.Join(tempDir, "server.crt")
	serverKeyPath := path.Join(tempDir, "server.key")
	serverOpts := &tlsutils.CertificateOptions{
		Hostname: "localhost",
	}
	err = tlsutils.GenerateServerCertToFile(caCert, caKey, serverCertPath, serverKeyPath, serverOpts)
	So(err, ShouldBeNil)

	// Generate client certificate
	clientCertPath := path.Join(tempDir, "client.crt")
	clientKeyPath := path.Join(tempDir, "client.key")
	err = tlsutils.GenerateClientCertToFile(caCert, caKey, clientCertPath, clientKeyPath, testCase.clientCertOptions)
	So(err, ShouldBeNil)

	// Set up server
	conf := config.New()
	port := test.GetFreePort()
	baseURL := test.GetSecureBaseURL(port)

	conf.HTTP.Port = port
	conf.HTTP.TLS = &config.TLSConfig{
		Cert:   serverCertPath,
		Key:    serverKeyPath,
		CACert: caCertPath,
	}
	conf.HTTP.Auth = &config.AuthConfig{
		MTLS: testCase.mtlsConfig,
	}

	// Set up access control
	repoPolicies := make([]config.Policy, 0)
	if len(testCase.allowedUsers) > 0 {
		repoPolicies = append(repoPolicies, config.Policy{
			Users:   testCase.allowedUsers,
			Actions: []string{"read", "create"},
		})
	}

	conf.HTTP.AccessControl = &config.AccessControlConfig{
		Repositories: config.Repositories{
			"**": config.PolicyGroup{
				AnonymousPolicy: make([]string, 0),
				Policies:        make([]config.Policy, 0),
			},
			"test-repo": config.PolicyGroup{
				Policies: repoPolicies,
			},
		},
	}
	conf.Storage.RootDirectory = t.TempDir()

	ctlr := api.NewController(conf)
	cm := test.NewControllerManager(ctlr)

	cm.StartAndWait(port)
	defer cm.StopServer()

	// Set up client
	caCertPEM, err := os.ReadFile(caCertPath)
	So(err, ShouldBeNil)

	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCertPEM)

	clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
	So(err, ShouldBeNil)

	client := resty.New()
	client.SetTLSClientConfig(&tls.Config{
		MinVersion:   tls.VersionTLS13,
		Certificates: []tls.Certificate{clientCert},
		RootCAs:      caCertPool,
	})

	// Make request
	resp, err := client.R().Get(baseURL + "/v2/test-repo/tags/list")
	So(err, ShouldBeNil)
	So(resp.StatusCode(), ShouldEqual, testCase.expectedStatus)
}

func TestExtractMTLSIdentity(t *testing.T) {
	testCases := []mTLSTestCase{
		// Positive tests - authentication should succeed
		{
			name: "CommonName",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser",
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"CommonName"},
			},
			allowedUsers:     []string{"testuser"},
			expectedIdentity: "testuser",
			expectedStatus:   http.StatusNotFound, // 404 means auth passed
			description:      "Extract identity from CommonName",
		},
		{
			name: "Subject",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser",
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Subject"},
			},
			allowedUsers:     []string{getExpectedSubjectDN("testuser")},
			expectedIdentity: getExpectedSubjectDN("testuser"),
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from Subject DN",
		},
		{
			name: "EmailSAN",
			clientCertOptions: &tlsutils.CertificateOptions{
				EmailAddresses: []string{"testuser@example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Email"},
			},
			allowedUsers:     []string{"testuser@example.com"},
			expectedIdentity: "testuser@example.com",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from Email SAN",
		},
		{
			name: "DNSNameSAN",
			clientCertOptions: &tlsutils.CertificateOptions{
				DNSNames: []string{"client.example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"DNSName"},
			},
			allowedUsers:     []string{"client.example.com"},
			expectedIdentity: "client.example.com",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from DNSName SAN",
		},
		{
			name: "URISAN",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
			},
			allowedUsers:     []string{"spiffe://example.org/workload/testuser"},
			expectedIdentity: "spiffe://example.org/workload/testuser",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from URI SAN",
		},
		{
			name: "URISANWithRegex",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
				URISANPattern:     "spiffe://example.org/workload/(.*)",
			},
			allowedUsers:     []string{"testuser"},
			expectedIdentity: "testuser",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from URI SAN with regex pattern",
		},
		{
			name: "FallbackChain",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser",
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Email", "DNSName", "CommonName"},
			},
			allowedUsers:     []string{"testuser"},
			expectedIdentity: "testuser",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity using fallback chain",
		},
		{
			name: "CaseInsensitive",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser",
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"commonname"}, // lowercase
			},
			allowedUsers:     []string{"testuser"},
			expectedIdentity: "testuser",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity with case-insensitive source name",
		},
		{
			name: "DNSANIndex1",
			clientCertOptions: &tlsutils.CertificateOptions{
				DNSNames: []string{"first.example.com", "second.example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"DNSName"},
				DNSANIndex:        1, // Use second DNS name
			},
			allowedUsers:     []string{"second.example.com"},
			expectedIdentity: "second.example.com",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from DNS SAN with index 1",
		},
		{
			name: "URISANIndex1",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{
					"spiffe://example.org/workload/first",
					"spiffe://example.org/workload/second",
				},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
				URISANIndex:       1, // Use second URI
			},
			allowedUsers:     []string{"spiffe://example.org/workload/second"},
			expectedIdentity: "spiffe://example.org/workload/second",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from URI SAN with index 1",
		},
		{
			name: "EmailSANIndex1",
			clientCertOptions: &tlsutils.CertificateOptions{
				EmailAddresses: []string{"first@example.com", "second@example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Email"},
				EmailSANIndex:     1, // Use second email
			},
			allowedUsers:     []string{"second@example.com"},
			expectedIdentity: "second@example.com",
			expectedStatus:   http.StatusNotFound,
			description:      "Extract identity from Email SAN with index 1",
		},
		// Negative tests - authentication should fail
		{
			name: "CommonNameNotAllowed",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser",
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"CommonName"},
			},
			allowedUsers:     []string{"otheruser"}, // Different user
			expectedIdentity: "testuser",
			expectedStatus:   http.StatusForbidden, // 403 means auth passed but access denied
			description:      "Authentication succeeds but user not in allowed list",
		},
		{
			name: "EmailSANNotAllowed",
			clientCertOptions: &tlsutils.CertificateOptions{
				EmailAddresses: []string{"testuser@example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Email"},
			},
			allowedUsers:     []string{"other@example.com"},
			expectedIdentity: "testuser@example.com",
			expectedStatus:   http.StatusForbidden,
			description:      "Email SAN extracted but user not in allowed list",
		},
		{
			name: "DNSNameSANNotAllowed",
			clientCertOptions: &tlsutils.CertificateOptions{
				DNSNames: []string{"client.example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"DNSName"},
			},
			allowedUsers:     []string{"other.example.com"},
			expectedIdentity: "client.example.com",
			expectedStatus:   http.StatusForbidden,
			description:      "DNSName SAN extracted but user not in allowed list",
		},
		{
			name: "URISANNotAllowed",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
			},
			allowedUsers:     []string{"spiffe://example.org/workload/other"},
			expectedIdentity: "spiffe://example.org/workload/testuser",
			expectedStatus:   http.StatusForbidden,
			description:      "URI SAN extracted but user not in allowed list",
		},
		{
			name: "URISANRegexNotAllowed",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
				URISANPattern:     "spiffe://example.org/workload/(.*)",
			},
			allowedUsers:     []string{"otheruser"},
			expectedIdentity: "testuser",
			expectedStatus:   http.StatusForbidden,
			description:      "URI SAN regex extracted but user not in allowed list",
		},
		{
			name:              "NoIdentitySource",
			clientCertOptions: &tlsutils.CertificateOptions{
				// No CN, no SANs
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"CommonName"},
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized, // 401 means auth failed
			description:      "No identity found in certificate",
		},
		{
			name:              "DefaultConfigWithoutCN",
			clientCertOptions: &tlsutils.CertificateOptions{
				// CommonName intentionally not set - no CN, no SANs
			},
			mtlsConfig:       nil,        // Default behavior (should default to CommonName)
			allowedUsers:     []string{}, // Not used - authentication fails before authorization is checked
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized, // 401 means auth failed - no CN to extract
			description: "Default config (CommonName) with certificate without CN - " +
				"authentication fails before authorization",
		},
		{
			name:              "FallbackChainAllFail",
			clientCertOptions: &tlsutils.CertificateOptions{
				// No CN, no SANs
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Email", "DNSName", "CommonName"},
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "Fallback chain fails when no identity attributes are available",
		},
		{
			name: "InvalidURISANIndex",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
				URISANIndex:       5, // Out of range
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "URI SAN index out of range",
		},
		{
			name: "InvalidDNSANIndex",
			clientCertOptions: &tlsutils.CertificateOptions{
				DNSNames: []string{"client.example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"DNSName"},
				DNSANIndex:        5, // Out of range
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "DNS SAN index out of range",
		},
		{
			name: "InvalidEmailSANIndex",
			clientCertOptions: &tlsutils.CertificateOptions{
				EmailAddresses: []string{"testuser@example.com"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"Email"},
				EmailSANIndex:     5, // Out of range
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "Email SAN index out of range",
		},
		{
			name: "URISANRegexNoMatch",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
				URISANPattern:     "spiffe://other.org/workload/(.*)", // Won't match
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "URI SAN regex pattern doesn't match",
		},
		{
			name: "NoURISANFound",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser", // Has CN but no URIs
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"}, // Try to extract from URL but cert has no URIs
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "No URI SAN found in certificate when URL is requested",
		},
		{
			name: "InvalidURISANPattern",
			clientCertOptions: &tlsutils.CertificateOptions{
				URIs: []string{"spiffe://example.org/workload/testuser"},
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"URI"},
				URISANPattern:     "[invalid(regex", // Invalid regex pattern
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "Invalid URI SAN regex pattern",
		},
		{
			name: "UnsupportedIdentitySource",
			clientCertOptions: &tlsutils.CertificateOptions{
				CommonName: "testuser",
			},
			mtlsConfig: &config.MTLSConfig{
				IdentityAttibutes: []string{"InvalidSource"}, // Unsupported source
			},
			allowedUsers:     []string{},
			expectedIdentity: "",
			expectedStatus:   http.StatusUnauthorized,
			description:      "Unsupported identity source",
		},
	}

	Convey("Test mTLS identity extraction", t, func() {
		for _, tc := range testCases {
			Convey(tc.description+" ("+tc.name+")", func() {
				runMTLSTest(t, tc)
			})
		}
	})
}

func TestMTLSAuthentication(t *testing.T) {
	// Create temporary directory for certificates
	tempDir := t.TempDir()

	// Generate CA certificate
	caCert, caKey, err := tlsutils.GenerateCACert()
	if err != nil {
		panic(err)
	}
	caCertPath := path.Join(tempDir, "ca.crt")
	err = os.WriteFile(caCertPath, caCert, 0o600)
	if err != nil {
		panic(err)
	}

	// Generate server certificate
	serverCertPath := path.Join(tempDir, "server.crt")
	serverKeyPath := path.Join(tempDir, "server.key")
	opts := &tlsutils.CertificateOptions{
		Hostname: "localhost",
	}
	err = tlsutils.GenerateServerCertToFile(caCert, caKey, serverCertPath, serverKeyPath, opts)
	if err != nil {
		panic(err)
	}

	// Generate valid client certificate for "testuser" user
	clientCertPath := path.Join(tempDir, "client.crt")
	clientKeyPath := path.Join(tempDir, "client.key")
	clientOpts := &tlsutils.CertificateOptions{
		CommonName: "testuser",
	}
	err = tlsutils.GenerateClientCertToFile(caCert, caKey, clientCertPath, clientKeyPath, clientOpts)
	if err != nil {
		panic(err)
	}

	// Generate self-signed client cert for "testuser" user
	selfSignedClientCertPath := path.Join(tempDir, "client-selfsigned.crt")
	selfSignedClientKeyPath := path.Join(tempDir, "client-selfsigned.key")
	selfSignedOpts := &tlsutils.CertificateOptions{
		CommonName: "testuser",
	}
	err = tlsutils.GenerateClientSelfSignedCertToFile(selfSignedClientCertPath, selfSignedClientKeyPath, selfSignedOpts)
	if err != nil {
		panic(err)
	}

	// Create htpasswd file with sample "httpuser"
	htpasswdPath := test.MakeHtpasswdFileFromString(t, test.GetBcryptCredString("httpuser", "httppass"))
	defer os.Remove(htpasswdPath)

	Convey("Test mTLS-only authentication", t, func() {
		// Set up server
		conf := config.New()
		port := test.GetFreePort()
		baseURL := test.GetSecureBaseURL(port)

		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}
		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Groups: config.Groups{
				"mtls-users": config.Group{
					Users: []string{"testuser"},
				},
			},
			Repositories: config.Repositories{
				"**": config.PolicyGroup{ // Default restrict all
					AnonymousPolicy: make([]string, 0),
					Policies:        make([]config.Policy, 0),
				},
				"test-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"testuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
			},
		}
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)
		cm := test.NewControllerManager(ctlr)

		cm.StartAndWait(port)
		defer cm.StopServer()

		// Test without client certificate - should fail
		caCertPEM, err := os.ReadFile(caCertPath)
		So(err, ShouldBeNil)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		client := resty.New()
		client.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS13})
		resp, err := client.R().Get(baseURL + "/v2/test-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)

		// Test with valid client certificate - should succeed
		clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		client = resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{clientCert},
			RootCAs:      caCertPool,
		})

		resp, err = client.R().Get(baseURL + "/v2/test-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 meaning we successfully passed auth

		// Test with self-signed client certificate - should fail
		selfSignedClientCert, err := tls.LoadX509KeyPair(selfSignedClientCertPath, selfSignedClientKeyPath)
		So(err, ShouldBeNil)

		client = resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{selfSignedClientCert},
			RootCAs:      caCertPool,
		})

		resp, err = client.R().Get(baseURL + "/v2/test-selfsigned-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
	})

	Convey("Test mTLS with basic auth and user/group access policies", t, func() {
		// Set up server
		conf := config.New()
		port := test.GetFreePort()
		baseURL := test.GetSecureBaseURL(port)

		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}
		conf.HTTP.Auth = &config.AuthConfig{
			HTPasswd: config.AuthHTPasswd{
				Path: htpasswdPath,
			},
		}
		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Groups: config.Groups{
				"mtls-users": config.Group{
					Users: []string{"testuser"},
				},
			},
			Repositories: config.Repositories{
				"**": config.PolicyGroup{ // Default restrict all
					AnonymousPolicy: make([]string, 0),
					Policies:        make([]config.Policy, 0),
				},
				"group-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Groups:  []string{"mtls-users"},
							Actions: []string{"read", "create"},
						},
					},
				},
				"test-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"testuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
				"htpasswd-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"httpuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
			},
		}
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)
		cm := test.NewControllerManager(ctlr)

		cm.StartAndWait(port)
		defer cm.StopServer()

		// Load server CA certificate
		caCertPEM, err := os.ReadFile(caCertPath)
		So(err, ShouldBeNil)

		// Load self-signed client certificate
		selfSignedClientCert, err := tls.LoadX509KeyPair(selfSignedClientCertPath, selfSignedClientKeyPath)
		So(err, ShouldBeNil)

		// Load valid client certificate with CN "testuser"
		clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		// Tests without client certificate
		client := resty.New()
		client.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS13})
		resp, err := client.R().SetBasicAuth("httpuser", "httppass").Get(baseURL + "/v2/htpasswd-repo/tags/list")
		// Test without client CA but with htpasswd credentials - should pass because of valid htpasswd credentials
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 meaning we successfully passed auth

		// Tests with self-signed (== non-acceptable by server) client certificate
		client = resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{selfSignedClientCert},
			RootCAs:      caCertPool,
		})

		// Test with self-signed client certificate - should still pass because of correct htpasswd auth
		resp, err = client.R().SetBasicAuth("httpuser", "httppass").Get(baseURL + "/v2/htpasswd-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 meaning we successfully passed auth

		// Tests with valid client certificate
		client = resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{clientCert},
			RootCAs:      caCertPool,
		})
		// Tests with valid client cert and creds - should fail with 403 due to no permissions for user from basic auth
		// This validates that identity from basic auth has higher priority over mTLS identity
		resp, err = client.R().SetBasicAuth("httpuser", "httppass").Get(baseURL + "/v2/test-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)

		// Test with correct auth credentials and different basic auth username from client certificate CN - should success
		// This validates that identity from basic auth has higher priority over mTLS identity
		resp, err = client.R().SetBasicAuth("httpuser", "httppass").Get(baseURL + "/v2/htpasswd-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 meaning we successfully passed auth

		// Should have access to test-repo for identity from client-cert
		resp, err = client.R().Get(baseURL + "/v2/test-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 meaning we successfully passed auth

		// Should not have access to other repos for identity from client-cert
		resp, err = client.R().Get(baseURL + "/v2/unauthorized-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)

		// Should have access to group-repo through group membership for identity from client-cert
		resp, err = client.R().Get(baseURL + "/v2/group-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 meaning we successfully passed auth
	})
}

func TestMTLSAuthenticationWithCertificateChain(t *testing.T) {
	// Create temporary directory for certificates
	tempDir := t.TempDir()

	Convey("Test mTLS with certificate chain - uses leaf certificate identity", t, func() {
		// Create certificate chain: Root CA -> Intermediate CA -> Client Certificate
		// Generate root CA
		rootCACert, rootCAKey, err := tlsutils.GenerateCACert()
		So(err, ShouldBeNil)
		rootCACertPath := path.Join(tempDir, "root-ca.crt")
		err = os.WriteFile(rootCACertPath, rootCACert, 0o600)
		So(err, ShouldBeNil)

		// Generate intermediate CA (signed by root CA)
		intermediateCAOpts := &tlsutils.CertificateOptions{
			CommonName: "Intermediate CA",
		}
		intermediateCACert, intermediateCAKeyPEM, err := tlsutils.GenerateIntermediateCACert(
			rootCACert, rootCAKey, intermediateCAOpts)
		So(err, ShouldBeNil)

		// Generate client certificate with CN signed by intermediate CA
		clientWithCNOpts := &tlsutils.CertificateOptions{
			CommonName: "clientuser",
		}
		clientCertWithCN, clientKeyWithCN, err := tlsutils.GenerateClientCert(
			intermediateCACert, intermediateCAKeyPEM, clientWithCNOpts)
		So(err, ShouldBeNil)

		// Generate client certificate without CN signed by intermediate CA
		clientWithoutCNOpts := &tlsutils.CertificateOptions{
			// No CommonName - empty to test that identity is not taken from intermediate CA
		}
		clientCertWithoutCN, clientKeyWithoutCNPEM, err := tlsutils.GenerateClientCert(
			intermediateCACert, intermediateCAKeyPEM, clientWithoutCNOpts)
		So(err, ShouldBeNil)

		// Generate server certificate signed by root CA for this test
		serverCertForChainPath := path.Join(tempDir, "server-chain.crt")
		serverKeyForChainPath := path.Join(tempDir, "server-chain.key")
		serverOpts := &tlsutils.CertificateOptions{
			Hostname: "localhost",
		}
		err = tlsutils.GenerateServerCertToFile(
			rootCACert, rootCAKey, serverCertForChainPath, serverKeyForChainPath, serverOpts)
		So(err, ShouldBeNil)

		// Set up server with root CA
		conf := config.New()
		port := test.GetFreePort()
		baseURL := test.GetSecureBaseURL(port)

		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertForChainPath,
			Key:    serverKeyForChainPath,
			CACert: rootCACertPath, // Server trusts root CA
		}
		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				"**": config.PolicyGroup{
					AnonymousPolicy: make([]string, 0),
					Policies:        make([]config.Policy, 0),
				},
				"client-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"clientuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
			},
		}
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)
		cm := test.NewControllerManager(ctlr)

		cm.StartAndWait(port)
		defer cm.StopServer()

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(rootCACert)

		// Test 1: Client cert with CN in chain - should use client cert CN, not intermediate CA CN
		clientCertWithCNPath := path.Join(tempDir, "client-with-cn.crt")
		clientKeyWithCNPath := path.Join(tempDir, "client-with-cn.key")
		err = os.WriteFile(clientCertWithCNPath, clientCertWithCN, 0o600)
		So(err, ShouldBeNil)
		err = os.WriteFile(clientKeyWithCNPath, clientKeyWithCN, 0o600)
		So(err, ShouldBeNil)

		// Create certificate chain file (client cert + intermediate CA)
		chainCertPath := path.Join(tempDir, "client-with-cn-chain.crt")
		err = tlsutils.WriteCertificateChainToFile(chainCertPath, clientCertWithCN, intermediateCACert)
		So(err, ShouldBeNil)

		// Load certificate chain
		clientCertChain, err := tls.LoadX509KeyPair(chainCertPath, clientKeyWithCNPath)
		So(err, ShouldBeNil)

		client := resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{clientCertChain},
			RootCAs:      caCertPool,
		})

		// Should succeed because client cert has CN "clientuser" which matches policy
		resp, err := client.R().Get(baseURL + "/v2/client-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) // 404 means auth passed

		// Test 2: Client cert without CN in chain - should fail, not use intermediate CA CN
		clientCertWithoutCNPath := path.Join(tempDir, "client-without-cn.crt")
		clientKeyWithoutCNPath := path.Join(tempDir, "client-without-cn.key")
		err = os.WriteFile(clientCertWithoutCNPath, clientCertWithoutCN, 0o600)
		So(err, ShouldBeNil)
		err = os.WriteFile(clientKeyWithoutCNPath, clientKeyWithoutCNPEM, 0o600)
		So(err, ShouldBeNil)

		// Create certificate chain file (client cert without CN + intermediate CA)
		chainCertWithoutCNPath := path.Join(tempDir, "client-without-cn-chain.crt")
		err = tlsutils.WriteCertificateChainToFile(chainCertWithoutCNPath, clientCertWithoutCN, intermediateCACert)
		So(err, ShouldBeNil)

		// Load certificate chain
		clientCertChainWithoutCN, err := tls.LoadX509KeyPair(chainCertWithoutCNPath, clientKeyWithoutCNPath)
		So(err, ShouldBeNil)

		client = resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{clientCertChainWithoutCN},
			RootCAs:      caCertPool,
		})

		// Should fail because client cert has no CN, even though intermediate CA has CN
		resp, err = client.R().Get(baseURL + "/v2/client-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
	})
}

func TestMTLSAuthenticationWithExpiredCertificate(t *testing.T) {
	// Create temporary directory for certificates
	tempDir := t.TempDir()

	Convey("Test mTLS authentication with expired certificate", t, func() {
		// Generate CA certificate
		caCert, caKey, err := tlsutils.GenerateCACert()
		So(err, ShouldBeNil)
		caCertPath := path.Join(tempDir, "ca.crt")
		err = os.WriteFile(caCertPath, caCert, 0o600)
		So(err, ShouldBeNil)

		// Generate server certificate
		serverCertPath := path.Join(tempDir, "server.crt")
		serverKeyPath := path.Join(tempDir, "server.key")
		opts := &tlsutils.CertificateOptions{
			Hostname: "localhost",
		}
		err = tlsutils.GenerateServerCertToFile(caCert, caKey, serverCertPath, serverKeyPath, opts)
		So(err, ShouldBeNil)

		// Generate expired client certificate (NotAfter is in the past)
		expiredClientCertPath := path.Join(tempDir, "client-expired.crt")
		expiredClientKeyPath := path.Join(tempDir, "client-expired.key")
		expiredOpts := &tlsutils.CertificateOptions{
			CommonName: "testuser",
			NotBefore:  time.Now().Add(-365 * 24 * time.Hour), // 1 year ago
			NotAfter:   time.Now().Add(-24 * time.Hour),       // 1 day ago (expired)
		}
		err = tlsutils.GenerateClientCertToFile(caCert, caKey, expiredClientCertPath, expiredClientKeyPath, expiredOpts)
		So(err, ShouldBeNil)

		// Set up server
		conf := config.New()
		port := test.GetFreePort()
		baseURL := test.GetSecureBaseURL(port)

		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}
		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				"**": config.PolicyGroup{
					AnonymousPolicy: make([]string, 0),
					Policies:        make([]config.Policy, 0),
				},
				"test-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"testuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
			},
		}
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)
		cm := test.NewControllerManager(ctlr)

		cm.StartAndWait(port)
		defer cm.StopServer()

		// Set up client with expired certificate
		caCertPEM, err := os.ReadFile(caCertPath)
		So(err, ShouldBeNil)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		expiredClientCert, err := tls.LoadX509KeyPair(expiredClientCertPath, expiredClientKeyPath)
		So(err, ShouldBeNil)

		client := resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{expiredClientCert},
			RootCAs:      caCertPool,
		})

		// Expired certificate should be rejected at TLS handshake level
		// The TLS stack will reject it before it reaches the application layer
		_, err = client.R().Get(baseURL + "/v2/test-repo/tags/list")
		// Error is expected - TLS handshake fails with expired certificate
		So(err, ShouldNotBeNil)
		So(err.Error(), ShouldContainSubstring, "expired certificate")
	})
}

func TestMTLSAuthenticationWithUnknownCA(t *testing.T) {
	// Create temporary directory for certificates
	tempDir := t.TempDir()

	Convey("Test mTLS authentication with certificate signed by unknown CA", t, func() {
		// Generate server CA and certificate
		serverCACert, serverCAKey, err := tlsutils.GenerateCACert()
		So(err, ShouldBeNil)
		serverCACertPath := path.Join(tempDir, "server-ca.crt")
		err = os.WriteFile(serverCACertPath, serverCACert, 0o600)
		So(err, ShouldBeNil)

		serverCertPath := path.Join(tempDir, "server.crt")
		serverKeyPath := path.Join(tempDir, "server.key")
		opts := &tlsutils.CertificateOptions{
			Hostname: "localhost",
		}
		err = tlsutils.GenerateServerCertToFile(serverCACert, serverCAKey, serverCertPath, serverKeyPath, opts)
		So(err, ShouldBeNil)

		// Generate a different CA (unknown to the server) and client certificate
		unknownCACert, unknownCAKey, err := tlsutils.GenerateCACert()
		So(err, ShouldBeNil)

		unknownClientCertPath := path.Join(tempDir, "client-unknown-ca.crt")
		unknownClientKeyPath := path.Join(tempDir, "client-unknown-ca.key")
		clientOpts := &tlsutils.CertificateOptions{
			CommonName: "testuser",
		}
		err = tlsutils.GenerateClientCertToFile(unknownCACert, unknownCAKey, unknownClientCertPath,
			unknownClientKeyPath, clientOpts)
		So(err, ShouldBeNil)

		// Set up server with server CA (doesn't know about unknown CA)
		conf := config.New()
		port := test.GetFreePort()
		baseURL := test.GetSecureBaseURL(port)

		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: serverCACertPath, // Server only trusts serverCACert, not unknownCACert
		}
		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				"**": config.PolicyGroup{
					AnonymousPolicy: make([]string, 0),
					Policies:        make([]config.Policy, 0),
				},
				"test-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"testuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
			},
		}
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)
		cm := test.NewControllerManager(ctlr)

		cm.StartAndWait(port)
		defer cm.StopServer()

		// Set up client with certificate signed by unknown CA
		serverCACertPEM, err := os.ReadFile(serverCACertPath)
		So(err, ShouldBeNil)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(serverCACertPEM)

		unknownClientCert, err := tls.LoadX509KeyPair(unknownClientCertPath, unknownClientKeyPath)
		So(err, ShouldBeNil)

		client := resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{unknownClientCert},
			RootCAs:      caCertPool,
		})

		// Certificate signed by unknown CA should be rejected at TLS handshake level
		// The TLS stack will reject it before it reaches the application layer
		_, err = client.R().Get(baseURL + "/v2/test-repo/tags/list")
		// Error is expected - TLS handshake fails with unknown certificate authority
		So(err, ShouldNotBeNil)
		So(err.Error(), ShouldContainSubstring, "unknown certificate authority")
	})
}

func TestMTLSAuthenticationWithMetaDBError(t *testing.T) {
	// Create temporary directory for certificates
	tempDir := t.TempDir()

	Convey("Test mTLS authentication with MetaDB.SetUserGroups error", t, func() {
		// Generate CA certificate
		caCert, caKey, err := tlsutils.GenerateCACert()
		So(err, ShouldBeNil)
		caCertPath := path.Join(tempDir, "ca.crt")
		err = os.WriteFile(caCertPath, caCert, 0o600)
		So(err, ShouldBeNil)

		// Generate server certificate
		serverCertPath := path.Join(tempDir, "server.crt")
		serverKeyPath := path.Join(tempDir, "server.key")
		opts := &tlsutils.CertificateOptions{
			Hostname: "localhost",
		}
		err = tlsutils.GenerateServerCertToFile(caCert, caKey, serverCertPath, serverKeyPath, opts)
		So(err, ShouldBeNil)

		// Generate valid client certificate for "testuser" user
		clientCertPath := path.Join(tempDir, "client.crt")
		clientKeyPath := path.Join(tempDir, "client.key")
		clientOpts := &tlsutils.CertificateOptions{
			CommonName: "testuser",
		}
		err = tlsutils.GenerateClientCertToFile(caCert, caKey, clientCertPath, clientKeyPath, clientOpts)
		So(err, ShouldBeNil)

		// Set up server
		conf := config.New()
		port := test.GetFreePort()
		baseURL := test.GetSecureBaseURL(port)

		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}
		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Groups: config.Groups{
				"mtls-users": config.Group{
					Users: []string{"testuser"},
				},
			},
			Repositories: config.Repositories{
				"**": config.PolicyGroup{
					AnonymousPolicy: make([]string, 0),
					Policies:        make([]config.Policy, 0),
				},
				"test-repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"testuser"},
							Actions: []string{"read", "create"},
						},
					},
				},
			},
		}
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)
		cm := test.NewControllerManager(ctlr)

		cm.StartAndWait(port)
		defer cm.StopServer()

		// Set up client with valid certificate
		caCertPEM, err := os.ReadFile(caCertPath)
		So(err, ShouldBeNil)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		client := resty.New()
		client.SetTLSClientConfig(&tls.Config{
			MinVersion:   tls.VersionTLS13,
			Certificates: []tls.Certificate{clientCert},
			RootCAs:      caCertPool,
		})

		// Mock MetaDB to return error on SetUserGroups
		ctlr.MetaDB = mocks.MetaDBMock{
			SetUserGroupsFn: func(ctx context.Context, groups []string) error {
				return ErrUnexpectedError
			},
		}

		// Should return 500 Internal Server Error due to MetaDB error
		resp, err := client.R().Get(baseURL + "/v2/test-repo/tags/list")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
	})
}

func TestMutualTLSAuthWithUserPermissions(t *testing.T) {
	Convey("Make a new controller", t, func() {
		caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath, caCertPEM := setupTestCerts(t)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		port := test.GetFreePort()
		baseURL := test.GetBaseURL(port)
		secureBaseURL := test.GetSecureBaseURL(port)

		resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})

		defer func() { resty.SetTLSClientConfig(nil) }()

		conf := config.New()
		conf.HTTP.Port = port

		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}

		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				test.AuthorizationAllRepos: config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{"testclient"},
							Actions: []string{"read"},
						},
					},
				},
			},
		}

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()

		cm := test.NewControllerManager(ctlr)
		cm.StartAndWait(port)

		defer cm.StopServer()

		resp, err := resty.R().Get(baseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		repoPolicy := conf.HTTP.AccessControl.Repositories[test.AuthorizationAllRepos]

		// setup TLS mutual auth
		cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		// Use separate resty client with certificates, because we cannot perform cleanup with resty.SetCertificates()
		client := resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:      caCertPool,
			MinVersion:   tls.VersionTLS12,
			Certificates: []tls.Certificate{cert},
		})

		// with client certs but without creds, should succeed
		resp, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		resp, err = client.R().Get(secureBaseURL + "/v2/_catalog")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		// with creds, should get expected status code
		resp, _ = client.R().Get(secureBaseURL)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// reading a repo should not get 403
		resp, err = client.R().Get(secureBaseURL + "/v2/repo/tags/list")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// without creds, writes should fail
		resp, err = client.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)

		// empty default authorization and give user the permission to create
		repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create")
		conf.HTTP.AccessControl.Repositories[test.AuthorizationAllRepos] = repoPolicy
		resp, err = client.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
	})
}

func TestTLSMutualAuth(t *testing.T) {
	Convey("Make a new controller", t, func() {
		caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath, caCertPEM := setupTestCerts(t)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		port := test.GetFreePort()
		baseURL := test.GetBaseURL(port)
		secureBaseURL := test.GetSecureBaseURL(port)

		conf := config.New()
		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()

		cm := test.NewControllerManager(ctlr)
		cm.StartAndWait(port)

		defer cm.StopServer()

		// access without any certificate settings
		client := resty.New()

		// accessing insecure HTTP site should fail
		resp, err := client.R().Get(baseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// without client certs and creds, should get certificate verification error
		_, err = client.R().Get(secureBaseURL)
		So(err, ShouldNotBeNil)

		// without client certs should fail auth
		_, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldNotBeNil)

		// Use resty client with certificates,
		client = resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:    caCertPool,
			MinVersion: tls.VersionTLS12,
		})

		// without client certs should fail auth
		resp, err = client.R().Get(secureBaseURL)
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// without client certs should fail auth
		resp, _ = client.R().Get(secureBaseURL + "/v2/")
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)

		username, seedUser := test.GenerateRandomString()
		password, seedPass := test.GenerateRandomString()
		ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")

		resp, err = client.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// with only creds, should get 401 because basic auth is disabled and no client certs are present
		// (Authorization header is ignored when basic auth is disabled, so auth falls through and fails)
		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)

		// setup TLS mutual auth
		cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		client = resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:      caCertPool,
			MinVersion:   tls.VersionTLS12,
			Certificates: []tls.Certificate{cert},
		})

		// with client certs but without creds, should succeed
		resp, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// with client certs and creds, should succeed via mTLS auth
		// (Authorization header is ignored when basic auth is disabled, so mTLS takes over)
		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)
	})
}

func TestTLSMutualAuthAllowReadAccess(t *testing.T) {
	Convey("Make a new controller", t, func() {
		caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath, caCertPEM := setupTestCerts(t)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		port := test.GetFreePort()
		baseURL := test.GetBaseURL(port)
		secureBaseURL := test.GetSecureBaseURL(port)

		// Use resty client with certificates,
		client := resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:    caCertPool,
			MinVersion: tls.VersionTLS12,
		})

		conf := config.New()
		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}

		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				test.AuthorizationAllRepos: config.PolicyGroup{
					AnonymousPolicy: []string{"read"},
				},
			},
		}

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()

		cm := test.NewControllerManager(ctlr)
		cm.StartAndWait(port)

		defer cm.StopServer()

		// accessing insecure HTTP site should fail
		resp, err := client.R().Get(baseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// without client certs and creds, reads are allowed
		resp, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		username, seedUser := test.GenerateRandomString()
		password, seedPass := test.GenerateRandomString()

		ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
		// with creds but without certs, reads are allowed via anonymous access
		// (Authorization header is ignored when basic auth is disabled, so the request succeeds via anonymous policy)
		resp, err = client.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		// without creds, writes should fail
		resp, err = client.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)

		// setup TLS mutual auth
		cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		// Use separate resty client with certificates, because we cannot perform cleanup with resty.SetCertificates()
		client = resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:      caCertPool,
			MinVersion:   tls.VersionTLS12,
			Certificates: []tls.Certificate{cert},
		})

		// with client certs but without creds, should succeed
		resp, _ = client.R().Get(secureBaseURL)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// with client certs but without creds, should succeed
		resp, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		// with client certs and creds, reads succeed via mTLS
		// (Authorization header is ignored when basic auth is disabled, so mTLS takes over)
		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		// with client certs, reads succeed via mTLS
		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)
	})
}

func TestTLSMutualAndBasicAuth(t *testing.T) {
	Convey("Make a new controller", t, func() {
		caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath, caCertPEM := setupTestCerts(t)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		username, seedUser := test.GenerateRandomString()
		password, seedPass := test.GenerateRandomString()

		htpasswdPath := test.MakeHtpasswdFileFromString(t, test.GetBcryptCredString(username, password))

		port := test.GetFreePort()
		baseURL := test.GetBaseURL(port)
		secureBaseURL := test.GetSecureBaseURL(port)

		resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})

		defer func() { resty.SetTLSClientConfig(nil) }()

		conf := config.New()
		conf.HTTP.Port = port
		conf.HTTP.Auth = &config.AuthConfig{
			HTPasswd: config.AuthHTPasswd{
				Path: htpasswdPath,
			},
		}
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()
		ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")

		cm := test.NewControllerManager(ctlr)
		cm.StartAndWait(port)

		defer cm.StopServer()

		// accessing insecure HTTP site should fail
		resp, err := resty.R().Get(baseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// without client certs and creds, should fail
		_, err = resty.R().Get(secureBaseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// with creds but without certs, should succeed
		_, err = resty.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// setup TLS mutual auth
		cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		// Use separate resty client with certificates, because we cannot perform cleanup with resty.SetCertificates()
		client := resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:      caCertPool,
			MinVersion:   tls.VersionTLS12,
			Certificates: []tls.Certificate{cert},
		})

		// with client certs but without creds, succeed because mTLS is used for auth when no auth headers provided
		resp, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		// with client certs and creds, should get expected status code
		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)
	})
}

func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) {
	Convey("Make a new controller", t, func() {
		caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath, caCertPEM := setupTestCerts(t)

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCertPEM)

		username, seedUser := test.GenerateRandomString()
		password, seedPass := test.GenerateRandomString()

		htpasswdPath := test.MakeHtpasswdFileFromString(t, test.GetBcryptCredString(username, password))

		port := test.GetFreePort()
		baseURL := test.GetBaseURL(port)
		secureBaseURL := test.GetSecureBaseURL(port)

		resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})

		defer func() { resty.SetTLSClientConfig(nil) }()

		conf := config.New()
		conf.HTTP.Port = port
		conf.HTTP.Auth = &config.AuthConfig{
			HTPasswd: config.AuthHTPasswd{
				Path: htpasswdPath,
			},
		}
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}

		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				test.AuthorizationAllRepos: config.PolicyGroup{
					AnonymousPolicy: []string{"read"},
				},
			},
		}

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()
		ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")

		cm := test.NewControllerManager(ctlr)
		cm.StartAndWait(port)

		defer cm.StopServer()

		// accessing insecure HTTP site should fail
		resp, err := resty.R().Get(baseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// without client certs and creds, should fail
		_, err = resty.R().Get(secureBaseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// with creds but without certs, should succeed
		_, err = resty.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)

		// setup TLS mutual auth
		cert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
		So(err, ShouldBeNil)

		// Use separate resty client with certificates, because we cannot perform cleanup with resty.SetCertificates()
		client := resty.New().SetTLSClientConfig(&tls.Config{
			RootCAs:      caCertPool,
			MinVersion:   tls.VersionTLS12,
			Certificates: []tls.Certificate{cert},
		})

		// with client certs but without creds, reads should succeed
		resp, err = client.R().Get(secureBaseURL + "/v2/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)

		// with only client certs, writes should fail with insufficient permissions
		resp, err = client.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
		So(err, ShouldBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)

		// with client certs and creds, should get expected status code
		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		resp, _ = client.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusOK)
	})
}

func TestTSLFailedReadingOfCACert(t *testing.T) {
	Convey("no permissions", t, func() {
		caCertPath, serverCertPath, serverKeyPath, _, _, _ := setupTestCerts(t)
		port := test.GetFreePort()
		conf := config.New()
		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: caCertPath,
		}

		err := os.Chmod(caCertPath, 0o000)
		defer func() {
			err := os.Chmod(caCertPath, 0o644)
			So(err, ShouldBeNil)
		}()
		So(err, ShouldBeNil)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()

		err = ctlr.Init()
		So(err, ShouldBeNil)

		errChan := make(chan error, 1)

		go func() {
			err = ctlr.Run()
			errChan <- err
		}()

		testTimeout := false

		select {
		case err := <-errChan:
			So(err, ShouldNotBeNil)
		case <-ctx.Done():
			testTimeout = true

			cancel()
		}

		So(testTimeout, ShouldBeFalse)
	})

	Convey("empty CACert", t, func() {
		badCACert := filepath.Join(t.TempDir(), "badCACert")
		err := os.WriteFile(badCACert, []byte(""), 0o600)
		So(err, ShouldBeNil)

		_, serverCertPath, serverKeyPath, _, _, _ := setupTestCerts(t)
		port := test.GetFreePort()
		conf := config.New()
		conf.HTTP.Port = port
		conf.HTTP.TLS = &config.TLSConfig{
			Cert:   serverCertPath,
			Key:    serverKeyPath,
			CACert: badCACert,
		}

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		ctlr := api.NewController(conf)
		ctlr.Config.Storage.RootDirectory = t.TempDir()

		err = ctlr.Init()
		So(err, ShouldBeNil)

		errChan := make(chan error, 1)

		go func() {
			err = ctlr.Run()
			errChan <- err
		}()

		testTimeout := false

		select {
		case err := <-errChan:
			So(err, ShouldNotBeNil)
		case <-ctx.Done():
			testTimeout = true

			cancel()
		}

		So(testTimeout, ShouldBeFalse)
	})
}
