// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddy

import (
	"bytes"
	"context"
	"crypto"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"maps"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"sync"
	"testing"
	"time"

	"github.com/caddyserver/certmagic"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"
)

var testCfg = []byte(`{
			"apps": {
				"http": {
					"servers": {
						"myserver": {
							"listen": ["tcp/localhost:8080-8084"],
							"read_timeout": "30s"
						},
						"yourserver": {
							"listen": ["127.0.0.1:5000"],
							"read_header_timeout": "15s"
						}
					}
				}
			}
		}
		`)

type testAdminPublicKey string

func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool {
	other, ok := x.(testAdminPublicKey)
	return ok && k == other
}

func TestUnsyncedConfigAccess(t *testing.T) {
	// each test is performed in sequence, so
	// each change builds on the previous ones;
	// the config is not reset between tests
	for i, tc := range []struct {
		method    string
		path      string // rawConfigKey will be prepended
		payload   string
		expect    string // JSON representation of what the whole config is expected to be after the request
		shouldErr bool
	}{
		{
			method:  "POST",
			path:    "",
			payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
			expect:  `{"foo": "bar", "list": ["a", "b", "c"]}`,
		},
		{
			method:  "POST",
			path:    "/foo",
			payload: `"jet"`,
			expect:  `{"foo": "jet", "list": ["a", "b", "c"]}`,
		},
		{
			method:  "POST",
			path:    "/bar",
			payload: `{"aa": "bb", "qq": "zz"}`,
			expect:  `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
		},
		{
			method: "DELETE",
			path:   "/bar/qq",
			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
		},
		{
			method:    "DELETE",
			path:      "/bar/qq",
			expect:    `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
			shouldErr: true,
		},
		{
			method:  "POST",
			path:    "/list",
			payload: `"e"`,
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
		},
		{
			method:  "PUT",
			path:    "/list/3",
			payload: `"d"`,
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
		},
		{
			method: "DELETE",
			path:   "/list/3",
			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
		},
		{
			method:  "PATCH",
			path:    "/list/3",
			payload: `"d"`,
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
		},
		{
			method:  "POST",
			path:    "/list/...",
			payload: `["e", "f", "g"]`,
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
		},
	} {
		err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)

		if tc.shouldErr && err == nil {
			t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
		}
		if !tc.shouldErr && err != nil {
			t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
		}

		// decode the expected config so we can do a convenient DeepEqual
		var expectedDecoded any
		err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
		if err != nil {
			t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
		}

		// make sure the resulting config is as we expect it
		if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
			t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
				i, expectedDecoded, rawCfg[rawConfigKey])
		}
	}
}

// TestLoadConcurrent exercises Load under concurrent conditions
// and is most useful under test with `-race` enabled.
func TestLoadConcurrent(t *testing.T) {
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Go(func() {
			_ = Load(testCfg, true)
		})
	}
	wg.Wait()
}

type fooModule struct {
	IntField int
	StrField string
}

func (fooModule) CaddyModule() ModuleInfo {
	return ModuleInfo{
		ID:  "foo",
		New: func() Module { return new(fooModule) },
	}
}
func (fooModule) Start() error { return nil }
func (fooModule) Stop() error  { return nil }

func TestETags(t *testing.T) {
	RegisterModule(fooModule{})

	if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
		t.Fatalf("loading: %s", err)
	}

	const key = "/" + rawConfigKey + "/apps/foo"

	// try update the config with the wrong etag
	err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
		t.Fatalf("expected precondition failed; got %v", err)
	}

	// get the etag
	hash := etagHasher()
	if err := readConfig(key, hash); err != nil {
		t.Fatalf("reading: %s", err)
	}

	// do the same update with the correct key
	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
	if err != nil {
		t.Fatalf("expected update to work; got %v", err)
	}

	// now try another update. The hash should no longer match and we should get precondition failed
	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
		t.Fatalf("expected precondition failed; got %v", err)
	}
}

func BenchmarkLoad(b *testing.B) {
	for b.Loop() {
		Load(testCfg, true)
	}
}

func TestAdminHandlerErrorHandling(t *testing.T) {
	initAdminMetrics()

	handler := adminHandler{
		mux: http.NewServeMux(),
	}

	handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		err := fmt.Errorf("test error")
		handler.handleError(w, r, err)
	}))

	req := httptest.NewRequest(http.MethodGet, "/error", nil)
	rr := httptest.NewRecorder()

	handler.ServeHTTP(rr, req)

	if rr.Code == http.StatusOK {
		t.Error("expected error response, got success")
	}

	var apiErr APIError
	if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
		t.Fatalf("decoding response: %v", err)
	}
	if apiErr.Message != "test error" {
		t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
	}
}

func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
	core, logs := observer.New(zap.InfoLevel)

	defaultLoggerMu.Lock()
	origLogger := defaultLogger.logger
	defaultLogger.logger = zap.New(core)
	defaultLoggerMu.Unlock()
	t.Cleanup(func() {
		defaultLoggerMu.Lock()
		defaultLogger.logger = origLogger
		defaultLoggerMu.Unlock()
	})

	handler := adminHandler{
		mux: http.NewServeMux(),
	}
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set("Authorization", "Bearer secret")
	req.Header.Set("Cookie", "session=secret")
	req.Header.Set("X-Test", "ok")
	rr := httptest.NewRecorder()

	handler.ServeHTTP(rr, req)

	if logs.Len() == 0 {
		t.Fatal("expected request log entry")
	}

	ctx := logs.All()[0].ContextMap()
	headers, ok := ctx["headers"].(map[string]any)
	if !ok {
		t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
	}

	if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
		t.Fatalf("expected redacted Authorization header, got %#v", got)
	}
	if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
		t.Fatalf("expected redacted Cookie header, got %#v", got)
	}
	if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
		t.Fatalf("expected X-Test header to remain visible, got %#v", got)
	}
}

func initAdminMetrics() {
	if adminMetrics.requestErrors != nil {
		prometheus.Unregister(adminMetrics.requestErrors)
	}
	if adminMetrics.requestCount != nil {
		prometheus.Unregister(adminMetrics.requestCount)
	}

	adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "caddy",
		Subsystem: "admin_http",
		Name:      "request_errors_total",
		Help:      "Number of errors that occurred handling admin endpoint requests",
	}, []string{"handler", "path", "method"})

	adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "caddy",
		Subsystem: "admin_http",
		Name:      "requests_total",
		Help:      "Count of requests to the admin endpoint",
	}, []string{"handler", "path", "code", "method"}) // Added code and method labels

	prometheus.MustRegister(adminMetrics.requestErrors)
	prometheus.MustRegister(adminMetrics.requestCount)
}

func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
	initAdminMetrics()

	cfg := &Config{
		Admin: &AdminConfig{
			Listen: "localhost:2019",
		},
	}

	// Build the admin handler directly (no listener active)
	addr, err := ParseNetworkAddress("localhost:2019")
	if err != nil {
		t.Fatalf("Failed to parse address: %v", err)
	}
	handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
	if err != nil {
		t.Fatalf("Failed to create admin handler: %v", err)
	}

	tests := []struct {
		name           string
		path           string
		method         string
		expectedStatus int
	}{
		{
			name:           "stop endpoint wrong method",
			path:           "/stop",
			method:         http.MethodGet,
			expectedStatus: http.StatusMethodNotAllowed,
		},
		{
			name:           "config endpoint wrong content-type",
			path:           "/config/",
			method:         http.MethodPost,
			expectedStatus: http.StatusBadRequest,
		},
		{
			name:           "config ID missing ID",
			path:           "/id/",
			method:         http.MethodGet,
			expectedStatus: http.StatusBadRequest,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
			rr := httptest.NewRecorder()

			handler.ServeHTTP(rr, req)

			if rr.Code != test.expectedStatus {
				t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
			}

			metricValue := testGetMetricValue(map[string]string{
				"path":    test.path,
				"handler": "admin",
				"method":  test.method,
			})
			if metricValue != 1 {
				t.Errorf("expected error metric to be incremented once, got %v", metricValue)
			}
		})
	}
}

func testGetMetricValue(labels map[string]string) float64 {
	promLabels := prometheus.Labels{}
	maps.Copy(promLabels, labels)

	metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
	if err != nil {
		return 0
	}

	pb := &dto.Metric{}
	metric.Write(pb)
	return pb.GetCounter().GetValue()
}

type mockRouter struct {
	routes []AdminRoute
}

func (m mockRouter) Routes() []AdminRoute {
	return m.routes
}

type mockModule struct {
	mockRouter
}

func (m *mockModule) CaddyModule() ModuleInfo {
	return ModuleInfo{
		ID: "admin.api.mock",
		New: func() Module {
			mm := &mockModule{
				mockRouter: mockRouter{
					routes: m.routes,
				},
			}
			return mm
		},
	}
}

func TestNewAdminHandlerRouterRegistration(t *testing.T) {
	originalModules := make(map[string]ModuleInfo)
	maps.Copy(originalModules, modules)
	defer func() {
		modules = originalModules
	}()

	mockRoute := AdminRoute{
		Pattern: "/mock",
		Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
			w.WriteHeader(http.StatusOK)
			return nil
		}),
	}

	mock := &mockModule{
		mockRouter: mockRouter{
			routes: []AdminRoute{mockRoute},
		},
	}
	RegisterModule(mock)

	addr, err := ParseNetworkAddress("localhost:2019")
	if err != nil {
		t.Fatalf("Failed to parse address: %v", err)
	}

	admin := &AdminConfig{
		EnforceOrigin: false,
	}
	handler, err := admin.newAdminHandler(addr, false, Context{})
	if err != nil {
		t.Fatalf("Failed to create admin handler: %v", err)
	}

	req := httptest.NewRequest("GET", "/mock", nil)
	req.Host = "localhost:2019"
	rr := httptest.NewRecorder()

	handler.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
		t.Logf("Response body: %s", rr.Body.String())
	}
}

type mockProvisionableRouter struct {
	mockRouter
	provisionErr error
	provisioned  bool
}

func (m *mockProvisionableRouter) Provision(Context) error {
	m.provisioned = true
	return m.provisionErr
}

type mockProvisionableModule struct {
	*mockProvisionableRouter
}

func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
	return ModuleInfo{
		ID: "admin.api.mock_provision",
		New: func() Module {
			mm := &mockProvisionableModule{
				mockProvisionableRouter: &mockProvisionableRouter{
					mockRouter:   m.mockRouter,
					provisionErr: m.provisionErr,
				},
			}
			return mm
		},
	}
}

func TestAdminRouterProvisioning(t *testing.T) {
	tests := []struct {
		name         string
		provisionErr error
		wantErr      bool
	}{
		{
			name:         "successful provisioning",
			provisionErr: nil,
			wantErr:      false,
		},
		{
			name:         "provisioning error",
			provisionErr: fmt.Errorf("provision failed"),
			wantErr:      true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			originalModules := make(map[string]ModuleInfo)
			maps.Copy(originalModules, modules)
			defer func() {
				modules = originalModules
			}()

			mockRoute := AdminRoute{
				Pattern: "/mock",
				Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
					return nil
				}),
			}

			// Create provisionable module
			mock := &mockProvisionableModule{
				mockProvisionableRouter: &mockProvisionableRouter{
					mockRouter: mockRouter{
						routes: []AdminRoute{mockRoute},
					},
					provisionErr: test.provisionErr,
				},
			}
			RegisterModule(mock)

			admin := &AdminConfig{}
			addr, err := ParseNetworkAddress("localhost:2019")
			if err != nil {
				t.Fatalf("Failed to parse address: %v", err)
			}

			_, err = admin.newAdminHandler(addr, false, Context{})

			if test.wantErr {
				if err == nil {
					t.Error("Expected error but got nil")
				}
			} else {
				if err != nil {
					t.Errorf("Expected no error but got: %v", err)
				}
			}
		})
	}
}

func TestAllowedOriginsUnixSocket(t *testing.T) {
	// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
	tests := []struct {
		name          string
		addr          NetworkAddress
		origins       []string
		expectOrigins []string
	}{
		{
			name: "unix socket with default origins",
			addr: NetworkAddress{
				Network: "unix",
				Host:    "/tmp/caddy.sock",
			},
			origins:       nil, // default origins
			expectOrigins: []string{},
		},
		{
			name: "unix socket with custom origins",
			addr: NetworkAddress{
				Network: "unix",
				Host:    "/tmp/caddy.sock",
			},
			origins: []string{"example.com"},
			expectOrigins: []string{
				"example.com",
			},
		},
		{
			name: "tcp socket on localhost gets all loopback addresses",
			addr: NetworkAddress{
				Network:   "tcp",
				Host:      "localhost",
				StartPort: 2019,
				EndPort:   2019,
			},
			origins: nil,
			expectOrigins: []string{
				"localhost:2019",
				"[::1]:2019",
				"127.0.0.1:2019",
			},
		},
	}

	for i, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			admin := AdminConfig{
				Origins: test.origins,
			}

			got := admin.allowedOrigins(test.addr)

			var gotOrigins []string
			for _, u := range got {
				gotOrigins = append(gotOrigins, u.Host)
			}

			if len(gotOrigins) != len(test.expectOrigins) {
				t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
				return
			}

			expectMap := make(map[string]struct{})
			for _, origin := range test.expectOrigins {
				expectMap[origin] = struct{}{}
			}

			gotMap := make(map[string]struct{})
			for _, origin := range gotOrigins {
				gotMap[origin] = struct{}{}
			}

			if !reflect.DeepEqual(expectMap, gotMap) {
				t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
			}
		})
	}
}

func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) {
	const authorizedKey testAdminPublicKey = "authorized"
	peerCert := &x509.Certificate{PublicKey: authorizedKey}

	tests := []struct {
		name        string
		allowedPath string
		requestPath string
		wantErr     bool
	}{
		{
			name:        "exact path",
			allowedPath: "/pki/ca/prod",
			requestPath: "/pki/ca/prod",
			wantErr:     false,
		},
		{
			name:        "subpath",
			allowedPath: "/pki/ca/prod",
			requestPath: "/pki/ca/prod/certificates",
			wantErr:     false,
		},
		{
			name:        "trailing slash subpath",
			allowedPath: "/pki/ca/prod/",
			requestPath: "/pki/ca/prod/certificates",
			wantErr:     false,
		},
		{
			name:        "sibling with shared prefix",
			allowedPath: "/pki/ca/prod",
			requestPath: "/pki/ca/prod-backup",
			wantErr:     true,
		},
		{
			name:        "same segment plus digit",
			allowedPath: "/pki/ca/prod",
			requestPath: "/pki/ca/prod1",
			wantErr:     true,
		},
		{
			name:        "root path",
			allowedPath: "/",
			requestPath: "/pki/ca/prod",
			wantErr:     false,
		},
	}

	for i, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			remote := RemoteAdmin{
				AccessControl: []*AdminAccess{
					{
						Permissions: []AdminPermissions{
							{
								Methods: []string{http.MethodGet},
								Paths:   []string{test.allowedPath},
							},
						},
						publicKeys: []crypto.PublicKey{authorizedKey},
					},
				},
			}

			req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil)
			req.TLS = &tls.ConnectionState{
				VerifiedChains: [][]*x509.Certificate{{peerCert}},
			}

			err := remote.enforceAccessControls(req)
			if test.wantErr {
				if err == nil {
					t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath)
					return
				}
				var apiErr APIError
				if !errors.As(err, &apiErr) {
					t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err)
					return
				}
				if apiErr.HTTPStatus != http.StatusForbidden {
					t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus)
				}
				return
			}

			if err != nil {
				t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err)
			}
		})
	}
}

func TestReplaceRemoteAdminServer(t *testing.T) {
	const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
vq+SH04xKhtFudVBAQ==`

	tests := []struct {
		name    string
		cfg     *Config
		wantErr bool
	}{
		{
			name:    "nil config",
			cfg:     nil,
			wantErr: false,
		},
		{
			name: "nil admin config",
			cfg: &Config{
				Admin: nil,
			},
			wantErr: false,
		},
		{
			name: "nil remote config",
			cfg: &Config{
				Admin: &AdminConfig{},
			},
			wantErr: false,
		},
		{
			name: "invalid listen address",
			cfg: &Config{
				Admin: &AdminConfig{
					Remote: &RemoteAdmin{
						Listen: "invalid:address",
					},
				},
			},
			wantErr: true,
		},
		{
			name: "valid config",
			cfg: &Config{
				Admin: &AdminConfig{
					Identity: &IdentityConfig{},
					Remote: &RemoteAdmin{
						Listen: "localhost:2021",
						AccessControl: []*AdminAccess{
							{
								PublicKeys:  []string{testCert},
								Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "invalid certificate",
			cfg: &Config{
				Admin: &AdminConfig{
					Identity: &IdentityConfig{},
					Remote: &RemoteAdmin{
						Listen: "localhost:2021",
						AccessControl: []*AdminAccess{
							{
								PublicKeys:  []string{"invalid-cert-data"},
								Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
							},
						},
					},
				},
			},
			wantErr: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctx := Context{
				Context: context.Background(),
				cfg:     test.cfg,
			}

			if test.cfg != nil {
				test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
			}

			if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
				identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
					GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
						return &certmagic.Config{}, nil
					},
				})
			}

			err := replaceRemoteAdminServer(ctx, test.cfg)

			if test.wantErr {
				if err == nil {
					t.Error("Expected error but got nil")
				}
			} else {
				if err != nil {
					t.Errorf("Expected no error but got: %v", err)
				}
			}

			// Clean up
			if remoteAdminServer != nil {
				_ = stopAdminServer(remoteAdminServer)
			}
		})
	}
}

type mockIssuer struct {
	configSet *certmagic.Config
}

func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
	return &certmagic.IssuedCertificate{
		Certificate: []byte(csr.Raw),
	}, nil
}

func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
	m.configSet = cfg
}

func (m *mockIssuer) IssuerKey() string {
	return "mock"
}

type mockIssuerModule struct {
	*mockIssuer
}

func (m *mockIssuerModule) CaddyModule() ModuleInfo {
	return ModuleInfo{
		ID: "tls.issuance.acme",
		New: func() Module {
			return &mockIssuerModule{mockIssuer: new(mockIssuer)}
		},
	}
}

func TestManageIdentity(t *testing.T) {
	originalModules := make(map[string]ModuleInfo)
	maps.Copy(originalModules, modules)
	defer func() {
		modules = originalModules
	}()

	RegisterModule(&mockIssuerModule{})

	certPEM := []byte(`-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----`)

	keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
...
-----END PRIVATE KEY-----`)

	tmpDir, err := os.MkdirTemp("", "TestManageIdentity-")
	if err != nil {
		t.Fatal(err)
	}
	testStorage := certmagic.FileStorage{Path: tmpDir}
	// Clean up the temp dir after the test finishes. Ensure any background
	// certificate maintenance is stopped first to avoid RemoveAll races.
	t.Cleanup(func() {
		if identityCertCache != nil {
			identityCertCache.Stop()
			identityCertCache = nil
		}
		// Give goroutines a moment to exit and release file handles.
		time.Sleep(50 * time.Millisecond)
		_ = os.RemoveAll(tmpDir)
	})

	err = testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
	if err != nil {
		t.Fatal(err)
	}
	err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name       string
		cfg        *Config
		wantErr    bool
		checkState func(*testing.T, *Config)
	}{
		{
			name: "nil config",
			cfg:  nil,
		},
		{
			name: "nil admin config",
			cfg: &Config{
				Admin: nil,
			},
		},
		{
			name: "nil identity config",
			cfg: &Config{
				Admin: &AdminConfig{},
			},
		},
		{
			name: "default issuer when none specified",
			cfg: &Config{
				Admin: &AdminConfig{
					Identity: &IdentityConfig{
						Identifiers: []string{"localhost"},
					},
				},
				storage: &testStorage,
			},
			checkState: func(t *testing.T, cfg *Config) {
				if len(cfg.Admin.Identity.issuers) == 0 {
					t.Error("Expected at least 1 issuer to be configured")
					return
				}
				if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
					t.Error("Expected mock issuer to be configured")
				}
			},
		},
		{
			name: "custom issuer",
			cfg: &Config{
				Admin: &AdminConfig{
					Identity: &IdentityConfig{
						Identifiers: []string{"localhost"},
						IssuersRaw: []json.RawMessage{
							json.RawMessage(`{"module": "acme"}`),
						},
					},
				},
				storage: &testStorage,
			},
			checkState: func(t *testing.T, cfg *Config) {
				if len(cfg.Admin.Identity.issuers) != 1 {
					t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
				}
				mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
				if !ok {
					t.Fatal("Expected mock issuer")
				}
				if mockIss.configSet == nil {
					t.Error("Issuer config was not set")
				}
			},
		},
		{
			name: "invalid issuer module",
			cfg: &Config{
				Admin: &AdminConfig{
					Identity: &IdentityConfig{
						Identifiers: []string{"localhost"},
						IssuersRaw: []json.RawMessage{
							json.RawMessage(`{"module": "doesnt_exist"}`),
						},
					},
				},
			},
			wantErr: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if identityCertCache != nil {
				// Reset the cert cache before each test
				identityCertCache.Stop()
				identityCertCache = nil
			}
			// Ensure any cache started by manageIdentity is stopped at the end
			defer func() {
				if identityCertCache != nil {
					identityCertCache.Stop()
					identityCertCache = nil
				}
			}()

			ctx := Context{
				Context:         context.Background(),
				cfg:             test.cfg,
				moduleInstances: make(map[string][]Module),
			}

			// If this test provided a FileStorage, set the package-level
			// testCertMagicStorageOverride so certmagicConfig will use it.
			if test.cfg != nil && test.cfg.storage != nil {
				testCertMagicStorageOverride = test.cfg.storage
				defer func() { testCertMagicStorageOverride = nil }()
			}

			err := manageIdentity(ctx, test.cfg)

			if test.wantErr {
				if err == nil {
					t.Error("Expected error but got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("Expected no error but got: %v", err)
			}

			if test.checkState != nil {
				test.checkState(t, test.cfg)
			}
		})
	}
}

func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
	rawCfg = map[string]any{
		rawConfigKey: map[string]any{
			"list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
		},
	}

	tests := []struct {
		name       string
		path       string
		wantOutput string
		wantErr    bool
	}{
		{name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
		{name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
		{name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
		{name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
		{name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
		{name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
		{name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
	}

	for i, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var gotOutput bytes.Buffer
			err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)

			if tc.wantErr {
				if err == nil {
					t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
				}
				return
			}

			if err != nil {
				t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
			}
			if gotOutput.String() != tc.wantOutput {
				t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
			}
		})
	}
}
