// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package unittest

import (
	"fmt"
	"io"
	"maps"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"slices"
	"strings"
	"testing"

	"gitea.dev/modules/log"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// MockServerOptions tweaks NewMockWebServer behavior.
type MockServerOptions struct {
	// Routes installs extra handlers on the mux before the fixture fallback;
	// more specific patterns win.
	Routes func(mux *http.ServeMux)
	// StripPrefix is trimmed from the request path before forwarding upstream,
	// useful when the client prepends a prefix the real upstream does not use
	// (e.g. go-github prepends "/api/v3").
	StripPrefix string
}

// NewMockWebServer returns a test HTTP server that records upstream responses on demand
// and replays them from disk on subsequent runs.
//
//   - liveMode=true: requests are forwarded to liveServerBaseURL and responses written as
//     fixture files under testDataDir.
//   - liveMode=false: responses come from existing fixture files.
//
// Fixture format: header lines ("Name: value"), a blank line, then the body. Before
// replay, occurrences of liveServerBaseURL in the body are swapped for the mock URL.
//
// The typical switch is an env var holding an API token; fixtures ship committed so the
// default run (no token) works offline.
//
//	token := os.Getenv("GITEA_TOKEN")
//	mock := NewMockWebServer(t, "https://gitea.com", fixtureDir, token != "")
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool, opts ...MockServerOptions) *httptest.Server {
	t.Helper()

	var opt MockServerOptions
	if len(opts) > 0 {
		opt = opts[0]
	}

	ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id", "set-cookie"}

	var mockURL string

	fallback := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reqPath := r.URL.EscapedPath()
		if r.URL.RawQuery != "" {
			reqPath += "?" + r.URL.RawQuery
		}
		log.Info("mock server: %s %s", r.Method, reqPath)

		fixturePath := fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(reqPath))
		if strings.Contains(r.URL.Path, ".git/") {
			fixturePath = fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(r.URL.Path))
		}

		if liveMode {
			require.NoError(t, os.MkdirAll(testDataDir, 0o755))

			liveURL := liveServerBaseURL + strings.TrimPrefix(reqPath, opt.StripPrefix)
			req, err := http.NewRequest(r.Method, liveURL, r.Body)
			require.NoError(t, err, "building upstream request to %s", liveURL)
			for name, values := range r.Header {
				if strings.EqualFold(name, "accept-encoding") {
					continue
				}
				for _, value := range values {
					req.Header.Add(name, value)
				}
			}

			resp, err := http.DefaultClient.Do(req)
			require.NoError(t, err, "upstream request to %s failed", liveURL)
			defer resp.Body.Close()
			assert.Less(t, resp.StatusCode, 400, "upstream %s returned status %d", liveURL, resp.StatusCode)

			out, err := os.Create(fixturePath)
			require.NoError(t, err, "creating fixture %s", fixturePath)
			defer out.Close()

			for _, name := range slices.Sorted(maps.Keys(resp.Header)) {
				if slices.Contains(ignoredHeaders, strings.ToLower(name)) {
					continue
				}
				for _, value := range resp.Header[name] {
					_, err := fmt.Fprintf(out, "%s: %s\n", name, value)
					require.NoError(t, err)
				}
			}
			_, err = out.WriteString("\n")
			require.NoError(t, err)

			_, err = io.Copy(out, resp.Body)
			require.NoError(t, err, "writing fixture body for %s", liveURL)
			require.NoError(t, out.Sync())
		}

		raw, err := os.ReadFile(fixturePath)
		require.NoError(t, err, "missing fixture: %s", fixturePath)

		replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL)
		headers, body, _ := strings.Cut(replayed, "\n\n")
		for line := range strings.SplitSeq(headers, "\n") {
			name, value, ok := strings.Cut(line, ": ")
			if !ok || strings.EqualFold(name, "Content-Length") {
				continue
			}
			w.Header().Set(name, value)
		}
		w.WriteHeader(http.StatusOK)
		_, err = w.Write([]byte(body))
		require.NoError(t, err)
	})

	mux := http.NewServeMux()
	if opt.Routes != nil {
		opt.Routes(mux)
	}
	mux.Handle("/", fallback)

	server := httptest.NewServer(mux)
	mockURL = server.URL
	t.Cleanup(server.Close)
	return server
}
