package integration

import (
	"fmt"
	"os"
	"sort"
	"strings"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"

	"github.com/treytartt/honeydue-api/internal/config"
	"github.com/treytartt/honeydue-api/internal/router"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

// routeKey is a comparable type for route matching: method + path
type routeKey struct {
	Method string
	Path   string
}

// TestRouteSpecContract verifies that registered Echo routes match the OpenAPI spec.
// It ensures bidirectional consistency:
//   - Every spec path has a corresponding registered route
//   - Every registered API route has a corresponding spec path
func TestRouteSpecContract(t *testing.T) {
	// --- Parse OpenAPI spec ---
	specRoutes := extractSpecRoutes(t)
	require.NotEmpty(t, specRoutes, "OpenAPI spec should have at least one route")

	// --- Set up Echo router ---
	db := testutil.SetupTestDB(t)
	cfg := &config.Config{}
	cfg.Server.Debug = true
	deps := &router.Dependencies{
		DB:     db,
		Config: cfg,
	}
	e := router.SetupRouter(deps)

	echoRoutes := extractEchoRoutes(e.Routes())
	require.NotEmpty(t, echoRoutes, "Echo router should have at least one route")

	// --- Bidirectional match ---
	t.Run("spec routes exist in router", func(t *testing.T) {
		var missing []string
		for _, sr := range specRoutes {
			if shouldSkipSpecRoute(sr.Path) {
				continue
			}
			if !containsRoute(echoRoutes, sr) {
				missing = append(missing, fmt.Sprintf("%s %s", sr.Method, sr.Path))
			}
		}
		if len(missing) > 0 {
			sort.Strings(missing)
			t.Errorf("OpenAPI spec defines routes not registered in Echo router:\n  %s",
				strings.Join(missing, "\n  "))
		}
	})

	t.Run("router routes exist in spec", func(t *testing.T) {
		var missing []string
		for _, er := range echoRoutes {
			if shouldSkipRouterRoute(er.Path) {
				continue
			}
			if !containsRoute(specRoutes, er) {
				missing = append(missing, fmt.Sprintf("%s %s", er.Method, er.Path))
			}
		}
		if len(missing) > 0 {
			sort.Strings(missing)
			t.Errorf("Echo routes not documented in OpenAPI spec:\n  %s",
				strings.Join(missing, "\n  "))
		}
	})
}

// extractSpecRoutes parses the OpenAPI YAML and returns normalized route keys.
// Spec paths use OpenAPI param format: /documents/{id}/
// These are returned as-is since Echo routes are converted to this format.
func extractSpecRoutes(t *testing.T) []routeKey {
	t.Helper()

	data, err := os.ReadFile("../../docs/openapi.yaml")
	require.NoError(t, err, "Failed to read openapi.yaml")

	var spec struct {
		Paths map[string]map[string]interface{} `yaml:"paths"`
	}
	require.NoError(t, yaml.Unmarshal(data, &spec), "Failed to parse openapi.yaml")

	var routes []routeKey
	for path, methods := range spec.Paths {
		for method := range methods {
			upper := strings.ToUpper(method)
			// Skip non-HTTP methods (parameters, summary, etc.)
			switch upper {
			case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS":
				routes = append(routes, routeKey{Method: upper, Path: path})
			}
		}
	}

	sort.Slice(routes, func(i, j int) bool {
		return routes[i].Path < routes[j].Path
	})
	return routes
}

// extractEchoRoutes returns normalized route keys from Echo's registered routes.
// Filters out admin, static, health, tracking, and internal routes.
func extractEchoRoutes(echoRoutes []*echo.Route) []routeKey {
	seen := make(map[routeKey]bool)
	var routes []routeKey

	for _, r := range echoRoutes {
		if shouldSkipRoute(r.Path, r.Method) {
			continue
		}

		// Strip /api prefix to match spec paths (spec server base is /api)
		path := strings.TrimPrefix(r.Path, "/api")

		// Normalize Echo :param to OpenAPI {param}
		path = normalizePathToOpenAPI(path)

		key := routeKey{Method: r.Method, Path: path}
		if !seen[key] {
			seen[key] = true
			routes = append(routes, key)
		}
	}

	sort.Slice(routes, func(i, j int) bool {
		return routes[i].Path < routes[j].Path
	})
	return routes
}

// normalizePathToOpenAPI converts Echo `:param` to OpenAPI `{param}` format.
func normalizePathToOpenAPI(path string) string {
	parts := strings.Split(path, "/")
	for i, part := range parts {
		if strings.HasPrefix(part, ":") {
			parts[i] = "{" + strings.TrimPrefix(part, ":") + "}"
		}
	}
	return strings.Join(parts, "/")
}

// shouldSkipRoute returns true for routes that are not part of the public API spec.
func shouldSkipRoute(path, method string) bool {
	// Skip non-API routes (static files, root page)
	if !strings.HasPrefix(path, "/api/") {
		return true
	}

	// Skip admin routes
	if strings.HasPrefix(path, "/api/admin") {
		return true
	}

	// Skip health check (internal, not in spec)
	if path == "/api/health/" {
		return true
	}

	// Skip email tracking (internal, not in spec)
	if strings.HasPrefix(path, "/api/track/") {
		return true
	}

	// Skip echo-internal routes (e.g., OPTIONS auto-generated by CORS)
	if method == "echo_route_not_found" {
		return true
	}

	return false
}

// shouldSkipSpecRoute returns true for spec routes that require optional services
// (e.g., storage/media routes require a non-nil StorageService which is not available in tests).
func shouldSkipSpecRoute(path string) bool {
	// Upload and media routes are conditionally registered (require StorageService)
	if strings.HasPrefix(path, "/uploads/") || strings.HasPrefix(path, "/media/") {
		return true
	}
	return false
}

// shouldSkipRouterRoute returns true for router routes that are intentionally
// not documented in the OpenAPI spec (internal aliases, webhooks, etc.).
func shouldSkipRouterRoute(path string) bool {
	skipPaths := map[string]bool{
		// Internal auth alias for mobile client compatibility
		"/auth/verify/": true,
		// Static data cache management (internal)
		"/static_data/refresh/": true,
		// Server-to-server webhook routes (called by Apple/Google, not mobile clients)
		"/subscription/webhook/apple/":  true,
		"/subscription/webhook/google/": true,
		// User management routes (internal/admin-facing, not in mobile API spec)
		"/users/":           true,
		"/users/profiles/":  true,
	}
	if skipPaths[path] {
		return true
	}
	// Skip /users/{id}/ pattern
	if strings.HasPrefix(path, "/users/{") {
		return true
	}
	return false
}

// containsRoute checks if a routeKey exists in a slice.
func containsRoute(routes []routeKey, target routeKey) bool {
	for _, r := range routes {
		if r.Method == target.Method && r.Path == target.Path {
			return true
		}
	}
	return false
}
