package server

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"regexp"
	"strings"
	"testing"

	"github.com/mem9-ai/drive9/pkg/openapi"
)

// TestOpenAPIRegistryCoversRoutes guards against registry drift by driving
// the assertion from the real route table in server.go: every mux.Handle /
// mux.HandleFunc pattern registered in NewWithConfig must be covered by the
// OpenAPI endpoint registry. A new route added to server.go without a
// registry entry fails this test; there is no second hand-maintained list to
// forget to update.
//
// Deliberately excluded non-business routes: /healthz, /metrics,
// /v1/auth/slock/* (OAuth flow) and /s3/* (local mock object store).
func TestOpenAPIRegistryCoversRoutes(t *testing.T) {
	src, err := os.ReadFile("server.go")
	if err != nil {
		t.Fatalf("read server.go: %v", err)
	}
	patterns := extractMuxPatterns(string(src))
	if len(patterns) == 0 {
		t.Fatalf("no mux patterns extracted from server.go; regex drift?")
	}
	registry := openapiRegistry()

	var missing []string
	for _, pattern := range patterns {
		if isExcludedRoute(pattern) {
			continue
		}
		if !registryCoversPattern(registry, pattern) {
			missing = append(missing, pattern)
		}
	}
	if len(missing) > 0 {
		t.Fatalf("OpenAPI registry does not cover mux patterns %v; "+
			"add endpoints for them in openapi_registry.go", missing)
	}
}

// muxPatternRe matches mux.Handle("...") and mux.HandleFunc("...") calls.
var muxPatternRe = regexp.MustCompile(`mux\.Handle(?:Func)?\("([^"]+)"`)

func extractMuxPatterns(src string) []string {
	seen := map[string]bool{}
	var out []string
	for _, m := range muxPatternRe.FindAllStringSubmatch(src, -1) {
		if !seen[m[1]] {
			seen[m[1]] = true
			out = append(out, m[1])
		}
	}
	return out
}

func isExcludedRoute(pattern string) bool {
	switch {
	case pattern == "/healthz", pattern == "/metrics":
		return true
	case strings.HasPrefix(pattern, "/v1/auth/slock"):
		return true
	case strings.HasPrefix(pattern, "/s3"):
		return true
	}
	return false
}

// registryCoversPattern reports whether any registry path template matches the
// mux pattern: trailing-slash patterns are prefixes, exact patterns also
// cover their sub-routes (mux.Handle("/v1/tokens") + Handle("/v1/tokens/")
// both serve /v1/tokens/{token_id}).
func registryCoversPattern(registry []openapi.Endpoint, pattern string) bool {
	for _, ep := range registry {
		if pathMatchesPattern(ep.Path, pattern) {
			return true
		}
	}
	return false
}

// pathMatchesPattern reports whether a registry path template satisfies a mux
// pattern: trailing-slash patterns are prefixes; exact patterns require an
// exact registry path, so a sub-route like /v1/uploads/initiate cannot
// satisfy the /v1/uploads route (mirrors ServeMux's exact-match semantics).
func pathMatchesPattern(path, pattern string) bool {
	if path == pattern {
		return true
	}
	if strings.HasSuffix(pattern, "/") {
		return strings.HasPrefix(path, pattern)
	}
	return false
}

// TestOpenAPIRegistryNoDuplicateDocs ensures each endpoint lists distinct
// document kinds (a duplicate entry would waste nothing but signals a
// copy-paste slip).
func TestOpenAPIRegistryNoDuplicateDocs(t *testing.T) {
	for _, ep := range openapiRegistry() {
		seen := map[openapi.DocKind]bool{}
		for _, kind := range ep.Docs {
			if seen[kind] {
				t.Fatalf("endpoint %s %s lists doc kind %s twice", ep.Method, ep.Path, kind)
			}
			seen[kind] = true
		}
		if len(ep.Docs) == 0 {
			t.Fatalf("endpoint %s %s lists no documents", ep.Method, ep.Path)
		}
	}
}

func TestOpenAPIRegistryDocumentsAppendLogAction(t *testing.T) {
	var appendLog *openapi.Endpoint
	registry := openapiRegistry()
	for i := range registry {
		if registry[i].Method == "POST" && registry[i].Path == "/v1/fs/{path}" {
			appendLog = &registry[i]
			break
		}
	}
	if appendLog == nil {
		t.Fatal("POST /v1/fs/{path} is not registered")
	}
	for _, want := range []struct {
		in   string
		name string
	}{
		{in: "query", name: "append-log"},
		{in: "header", name: "X-Dat9-Expected-Revision"},
		{in: "header", name: "X-Dat9-Expected-Size"},
	} {
		found := false
		for _, got := range appendLog.Parameters {
			if got.In == want.in && got.Name == want.name {
				found = true
				break
			}
		}
		if !found {
			t.Fatalf("append-log contract missing %s parameter %q", want.in, want.name)
		}
	}
	if !strings.Contains(appendLog.Description, "append-log requires Content-Length") {
		t.Fatalf("append-log contract description = %q", appendLog.Description)
	}
}

func TestOpenAPIDocumentsAppendLogWireContract(t *testing.T) {
	docs, err := OpenAPIDocuments("")
	if err != nil {
		t.Fatal(err)
	}
	committed, err := os.ReadFile("../../docs/api/data-plane.openapi.json")
	if err != nil {
		t.Fatal(err)
	}
	for name, raw := range map[string][]byte{"generated": docs[openapi.DataPlane], "committed": committed} {
		t.Run(name, func(t *testing.T) {
			var doc map[string]any
			if err := json.Unmarshal(raw, &doc); err != nil {
				t.Fatal(err)
			}
			post := doc["paths"].(map[string]any)["/v1/fs/{path}"].(map[string]any)["post"].(map[string]any)
			t.Run("binary and JSON request bodies", func(t *testing.T) {
				body := post["requestBody"].(map[string]any)
				// Other selectors, including mkdir, do not require a body.
				if body["required"] != false {
					t.Fatal("shared POST body must remain optional")
				}
				content := body["content"].(map[string]any)
				binary, ok := content["application/octet-stream"].(map[string]any)
				if !ok {
					t.Fatal("append-log binary media type is missing")
				}
				schema := binary["schema"].(map[string]any)
				if schema["type"] != "string" || schema["format"] != "binary" {
					t.Fatalf("binary schema = %v", schema)
				}
				jsonContent, ok := content["application/json"].(map[string]any)
				if !ok || jsonContent["schema"].(map[string]any)["oneOf"] == nil {
					t.Fatal("existing action-specific JSON body schemas must remain available")
				}
			})
			t.Run("structured client errors", func(t *testing.T) {
				response := post["responses"].(map[string]any)["4XX"].(map[string]any)
				schema := response["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)
				if schemaRef, ok := schema["$ref"].(string); ok {
					schema = doc["components"].(map[string]any)["schemas"].(map[string]any)[strings.TrimPrefix(schemaRef, "#/components/schemas/")].(map[string]any)
				}
				props := schema["properties"].(map[string]any)
				for _, code := range []string{"append_log_conflict", "append_log_rebased", "append_log_unsupported"} {
					w := httptest.NewRecorder()
					errJSONCode(w, http.StatusConflict, code, "append-log error")
					var wire map[string]string
					if err := json.Unmarshal(w.Body.Bytes(), &wire); err != nil {
						t.Fatal(err)
					}
					for field := range wire {
						property, ok := props[field].(map[string]any)
						if !ok || property["type"] != "string" {
							t.Fatalf("runtime error field %q is missing from schema: %v", field, schema)
						}
					}
				}
				// Authentication and other POST actions can return only {error}.
				if required := schema["required"].([]any); len(required) != 1 || required[0] != "error" {
					t.Fatalf("required error fields = %v; code must be optional for non-append-log errors", required)
				}
			})
		})
	}
}
