package main

import (
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"testing"

	"github.com/gesellix/bose-soundtouch/pkg/service/handlers"
	"github.com/go-chi/chi/v5"
)

// frozenFirstSegments are the top-level path prefixes that belong to the frozen
// speaker / app contract (category 1a/1b in
// docs/content/docs/architecture/API-ROUTE-LAYOUT.md). Routes under these must
// not change shape across the issue #451 refactor, so each should have at least
// one .http contract test (the suite under tests/integration/http-client/, run
// by `make test-http-client`). Movable surfaces (/setup, /mgmt, /web) and infra
// (/, /health, /docs, /favicon.ico) are intentionally excluded.
var frozenFirstSegments = map[string]bool{
	"streaming": true,
	"accounts":  true,
	"customer":  true,
	"bmx":       true,
	"bmx-icons": true,
	"core02":    true,
	"oauth":     true,
	"custom":    true,
	"media":     true,
	"updates":   true,
	"v1":        true,
	"alexa":     true,
	"ced":       true,
}

func coverageFirstSegment(p string) string {
	p = strings.TrimPrefix(p, "/")
	if i := strings.IndexByte(p, '/'); i >= 0 {
		return p[:i]
	}

	return p
}

// patternToRegexp converts a chi route pattern into an anchored regexp:
// `{param}` becomes a single path segment (`[^/]+`) and `*` becomes `.*`.
func patternToRegexp(pattern string) *regexp.Regexp {
	var b strings.Builder

	b.WriteString("^")

	for i, seg := range strings.Split(pattern, "/") {
		if i > 0 {
			b.WriteString("/")
		}

		switch {
		case seg == "*":
			b.WriteString(".*")
		case strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}"):
			b.WriteString("[^/]+")
		default:
			b.WriteString(regexp.QuoteMeta(seg))
		}
	}

	b.WriteString("$")

	return regexp.MustCompile(b.String())
}

// loadHTTPClientRequests extracts (method, path) pairs from every .http file in
// the integration suite. `{{host}}` is stripped (leaving a leading `/`), query
// strings are dropped, and `{{var}}` template segments are left intact (they
// contain no slash, so they match a `[^/]+` route segment).
func loadHTTPClientRequests(t *testing.T, dir string) [][2]string {
	t.Helper()

	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("read http-client dir %s: %v", dir, err)
	}

	reqLine := regexp.MustCompile(`^\s*(GET|POST|PUT|DELETE|PATCH|HEAD)\s+(\S+)`)

	var out [][2]string

	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".http") {
			continue
		}

		data, err := os.ReadFile(filepath.Join(dir, e.Name()))
		if err != nil {
			t.Fatalf("read %s: %v", e.Name(), err)
		}

		for _, line := range strings.Split(string(data), "\n") {
			m := reqLine.FindStringSubmatch(line)
			if m == nil {
				continue
			}

			url := strings.ReplaceAll(m[2], "{{host}}", "")
			if i := strings.IndexByte(url, '?'); i >= 0 {
				url = url[:i]
			}

			if !strings.HasPrefix(url, "/") {
				continue
			}

			out = append(out, [2]string{m[1], url})
		}
	}

	return out
}

// TestFrozenRouteContractCoverage enforces that every frozen-contract route the
// service registers is exercised by at least one .http integration test. The
// set of *uncovered* frozen routes is golden-filed: adding a new frozen route
// without a test (or adding a test that newly covers one) changes the set and
// fails this test, forcing a conscious update of the golden file. It is the
// machine-checked companion to tests/integration/http-client/COVERAGE.md.
func TestFrozenRouteContractCoverage(t *testing.T) {
	server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true)
	r := setupRouter(server, nil, nil)

	httpRequests := loadHTTPClientRequests(t, filepath.Join("..", "..", "tests", "integration", "http-client"))

	// Only the request methods the contract suite actually exercises. Routes
	// registered via chi HandleFunc carry every method (CONNECT/TRACE/...); those
	// extra verbs are noise for coverage purposes.
	meaningfulMethods := map[string]bool{
		http.MethodGet: true, http.MethodPost: true, http.MethodPut: true, http.MethodDelete: true,
	}

	var uncovered []string

	walkFunc := func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
		if !meaningfulMethods[method] {
			return nil
		}

		if !frozenFirstSegments[coverageFirstSegment(route)] {
			return nil
		}

		re := patternToRegexp(route)
		for _, req := range httpRequests {
			if req[0] == method && re.MatchString(req[1]) {
				return nil
			}
		}

		uncovered = append(uncovered, fmt.Sprintf("%-7s %s", method, route))

		return nil
	}

	if err := chi.Walk(r, walkFunc); err != nil {
		t.Fatalf("walk routes: %v", err)
	}

	sort.Strings(uncovered)
	output := strings.Join(uncovered, "\n") + "\n"

	const goldenPath = "testdata/frozen_routes_uncovered.txt"

	actualPath := "testdata/frozen_routes_uncovered.actual.txt"
	if err := os.WriteFile(actualPath, []byte(output), 0644); err != nil {
		t.Fatalf("write actual: %v", err)
	}

	golden, err := os.ReadFile(goldenPath)
	if os.IsNotExist(err) {
		if err := os.WriteFile(goldenPath, []byte(output), 0644); err != nil {
			t.Fatalf("create golden: %v", err)
		}

		t.Logf("created golden %s with %d uncovered frozen routes", goldenPath, len(uncovered))

		return
	}

	if err != nil {
		t.Fatalf("read golden: %v", err)
	}

	if string(golden) != output {
		t.Errorf("Frozen-route contract coverage changed.\n"+
			"A frozen route either lost its .http test or a new one was added without one.\n"+
			"Review and, if intended, update %s from %s.", goldenPath, actualPath)
	}
}
