package integration

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

	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
)

// TestKMPSpecContract verifies that KMP API clients (*Api.kt) match the OpenAPI spec.
// It ensures bidirectional consistency:
//   - Every spec endpoint (minus exclusions) has a KMP implementation
//   - Every KMP endpoint (after alias resolution) exists in the spec
func TestKMPSpecContract(t *testing.T) {
	// --- Parse OpenAPI spec ---
	specRoutes := extractSpecRoutesForKMP(t)
	require.NotEmpty(t, specRoutes, "OpenAPI spec should have at least one route")

	// --- Extract KMP routes ---
	kmpRoutes := extractKMPRoutes(t)
	require.NotEmpty(t, kmpRoutes, "KMP API clients should have at least one route")

	t.Logf("Spec routes: %d, KMP routes: %d", len(specRoutes), len(kmpRoutes))

	// --- Direction 1: Every spec endpoint covered by KMP ---
	t.Run("spec endpoints covered by KMP", func(t *testing.T) {
		var missing []string
		for _, sr := range specRoutes {
			if specEndpointsKMPSkips[sr] {
				continue
			}
			if !containsRoute(kmpRoutes, sr) {
				missing = append(missing, fmt.Sprintf("%s %s", sr.Method, sr.Path))
			}
		}
		if len(missing) > 0 {
			sort.Strings(missing)
			t.Errorf("OpenAPI spec defines endpoints not implemented in KMP:\n  %s",
				strings.Join(missing, "\n  "))
		}
	})

	// --- Direction 2: Every KMP endpoint exists in spec ---
	t.Run("KMP endpoints exist in spec", func(t *testing.T) {
		var missing []string
		for _, kr := range kmpRoutes {
			if !containsRoute(specRoutes, kr) {
				missing = append(missing, fmt.Sprintf("%s %s", kr.Method, kr.Path))
			}
		}
		if len(missing) > 0 {
			sort.Strings(missing)
			t.Errorf("KMP API clients call endpoints not defined in OpenAPI spec:\n  %s",
				strings.Join(missing, "\n  "))
		}
	})
}

// specEndpointsKMPSkips are spec endpoints that KMP intentionally does not implement.
// All paths must use normalized form ({_} for all path params) to match extractSpecRoutesForKMP output.
var specEndpointsKMPSkips = map[routeKey]bool{
	// Upload endpoints — KMP embeds file uploads in domain-specific multipart calls
	{Method: "POST", Path: "/uploads/image/"}:            true,
	{Method: "POST", Path: "/uploads/document/"}:         true,
	{Method: "POST", Path: "/uploads/completion-file/"}:  true,
	{Method: "POST", Path: "/uploads/completion/"}:       true,
	{Method: "DELETE", Path: "/uploads/"}:                 true,
	// Media proxy — KMP uses dynamic URLs from API responses
	{Method: "GET", Path: "/media/{_}"}:                   true, // /media/{path}
	{Method: "GET", Path: "/media/document/{_}"}:          true,
	{Method: "GET", Path: "/media/document-image/{_}"}:    true,
	{Method: "GET", Path: "/media/completion-image/{_}"}:  true,
	// PUT/PATCH variants where KMP uses only one method
	{Method: "PUT", Path: "/contractors/{_}/"}:             true, // KMP uses PATCH
	{Method: "PUT", Path: "/documents/{_}/"}:               true, // KMP uses PATCH
	{Method: "PATCH", Path: "/residences/{_}/"}:            true, // KMP uses PUT
	{Method: "PATCH", Path: "/tasks/{_}/completions/"}:     true, // KMP uses PUT on /task-completions/{_}/
	{Method: "PATCH", Path: "/auth/profile/"}:              true, // KMP uses PUT
	// Task action endpoints where KMP uses PATCH on main resource or different endpoint
	{Method: "POST", Path: "/tasks/{_}/mark-in-progress/"}: true, // KMP uses PATCH tasks/{_}/
	{Method: "POST", Path: "/tasks/{_}/quick-complete/"}:   true, // KMP uses POST /task-completions/
	// Subscription endpoints handled client-side or via different path
	{Method: "POST", Path: "/subscription/cancel/"}:                true, // Handled by App Store / Play Store
	{Method: "GET", Path: "/subscription/"}:                        true, // KMP uses /subscription/status/
	{Method: "GET", Path: "/subscription/upgrade-trigger/{_}/"}:    true, // KMP uses list endpoint
	// Auth endpoints not yet implemented in KMP
	{Method: "POST", Path: "/auth/resend-verification/"}: true,
	// Document warranty endpoint — KMP filters via query params on /documents/
	{Method: "GET", Path: "/documents/warranties/"}: true,
	// Device management — KMP uses different endpoints
	{Method: "GET", Path: "/notifications/devices/"}:          true, // KMP doesn't list devices
	{Method: "POST", Path: "/notifications/devices/"}:         true, // KMP uses /notifications/devices/register/
	{Method: "POST", Path: "/notifications/devices/unregister/"}: true, // KMP uses DELETE on device ID
	{Method: "PATCH", Path: "/notifications/preferences/"}:    true, // KMP uses PUT
	// Regional templates — not yet implemented in KMP (planned)
	{Method: "GET", Path: "/tasks/templates/by-region/"}: true,
	// Stripe web-only and server-to-server endpoints — not implemented in mobile KMP
	{Method: "POST", Path: "/subscription/checkout/"}:         true, // Web-only (Stripe Checkout)
	{Method: "POST", Path: "/subscription/portal/"}:           true, // Web-only (Stripe Customer Portal)
	{Method: "POST", Path: "/subscription/webhook/stripe/"}:   true, // Server-to-server (Stripe webhook)
}

// kmpRouteAliases maps KMP paths to their canonical spec paths.
// Applied after extraction but before spec comparison.
// Currently empty — all KMP paths match spec paths after the /auth/verify/ → /auth/verify-email/ fix.
var kmpRouteAliases = map[routeKey]routeKey{}

// kmpDynamicExpansions maps dynamic route patterns to concrete actions.
// Used for TaskApi.postTaskAction which builds paths like /tasks/$id/$action/
var kmpDynamicExpansions = map[routeKey][]string{
	{Method: "POST", Path: "/tasks/{_}/{_}/"}: {"cancel", "uncancel", "archive", "unarchive"},
}

// Regex patterns for extracting HTTP calls from KMP *Api.kt files.
var (
	// Standard Ktor calls: client.get("$baseUrl/path/")
	reStandardCall = regexp.MustCompile(`client\.(get|post|put|patch|delete)\(\s*"?\$baseUrl(/[^")\s]+)"?`)
	// Multipart calls: submitFormWithBinaryData(url = "$baseUrl/path/", ...)
	reMultipartCall = regexp.MustCompile(`submitFormWithBinaryData\(\s*(?:\n\s*)?url\s*=\s*"\$baseUrl(/[^"]+)"`)
)

// extractKMPRoutes scans KMP *Api.kt files and returns normalized route keys.
func extractKMPRoutes(t *testing.T) []routeKey {
	t.Helper()

	// KMP source directory relative to this test file
	kmpDir := filepath.Join("..", "..", "..", "HoneyDueKMM", "composeApp", "src", "commonMain", "kotlin", "com", "example", "honeydue", "network")

	// Verify directory exists — skip in CI where KMP repo may not be checked out
	info, err := os.Stat(kmpDir)
	if os.IsNotExist(err) {
		t.Skipf("KMP network directory not found at %s (expected in monorepo layout)", kmpDir)
	}
	require.NoError(t, err, "Failed to stat KMP network directory at %s", kmpDir)
	require.True(t, info.IsDir(), "KMP network path is not a directory")

	// Find all *Api.kt files
	matches, err := filepath.Glob(filepath.Join(kmpDir, "*Api.kt"))
	require.NoError(t, err, "Failed to glob *Api.kt files")
	require.NotEmpty(t, matches, "No *Api.kt files found in %s", kmpDir)

	seen := make(map[routeKey]bool)
	var routes []routeKey

	for _, file := range matches {
		data, err := os.ReadFile(file)
		require.NoError(t, err, "Failed to read %s", file)
		content := string(data)

		// Extract standard HTTP calls
		for _, match := range reStandardCall.FindAllStringSubmatch(content, -1) {
			method := strings.ToUpper(match[1])
			path := normalizeKMPPath(match[2])
			key := routeKey{Method: method, Path: path}
			if !seen[key] {
				seen[key] = true
				routes = append(routes, key)
			}
		}

		// Extract multipart calls (these are always POST)
		for _, match := range reMultipartCall.FindAllStringSubmatch(content, -1) {
			path := normalizeKMPPath(match[1])
			key := routeKey{Method: "POST", Path: path}
			if !seen[key] {
				seen[key] = true
				routes = append(routes, key)
			}
		}
	}

	// Expand dynamic routes (e.g., /tasks/$id/$action/ → cancel, uncancel, etc.)
	routes = expandDynamicRoutes(routes, seen)

	// Resolve aliases (e.g., /auth/verify/ → /auth/verify-email/)
	routes = resolveAliases(routes)

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

// normalizeKMPPath converts KMP string interpolation paths to a generic param format.
// Replaces $variable segments with {_} to match normalized spec paths.
// Example: /tasks/$id/$action/ → /tasks/{_}/{_}/
func normalizeKMPPath(path string) string {
	parts := strings.Split(path, "/")
	for i, part := range parts {
		if strings.HasPrefix(part, "$") {
			parts[i] = "{_}"
		}
	}
	return strings.Join(parts, "/")
}

// normalizeSpecPath converts OpenAPI {paramName} segments to {_} for comparison.
// Example: /tasks/{id}/completions/ → /tasks/{_}/completions/
// Special case: /media/{path} stays as /media/{path} (wildcard, not a segment param)
func normalizeSpecPath(path string) string {
	parts := strings.Split(path, "/")
	for i, part := range parts {
		if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
			parts[i] = "{_}"
		}
	}
	return strings.Join(parts, "/")
}

// expandDynamicRoutes expands routes with multiple path params into concrete action routes.
// For example, POST /tasks/{_}/{_}/ becomes POST /tasks/{_}/cancel/, etc.
func expandDynamicRoutes(routes []routeKey, seen map[routeKey]bool) []routeKey {
	var expanded []routeKey
	for _, r := range routes {
		if actions, ok := kmpDynamicExpansions[r]; ok {
			// Replace the last {_} segment with each concrete action
			for _, action := range actions {
				// Build path: replace last {_} with action name
				path := strings.TrimSuffix(r.Path, "{_}/") + action + "/"
				key := routeKey{Method: r.Method, Path: path}
				if !seen[key] {
					seen[key] = true
					expanded = append(expanded, key)
				}
			}
			// Don't include the generic pattern itself
			continue
		}
		expanded = append(expanded, r)
	}
	return expanded
}

// resolveAliases replaces KMP routes with their canonical spec equivalents.
func resolveAliases(routes []routeKey) []routeKey {
	result := make([]routeKey, 0, len(routes))
	for _, r := range routes {
		if canonical, ok := kmpRouteAliases[r]; ok {
			result = append(result, canonical)
		} else {
			result = append(result, r)
		}
	}
	return result
}

// extractSpecRoutesForKMP parses openapi.yaml and returns routes normalized for KMP comparison.
func extractSpecRoutesForKMP(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")

	seen := make(map[routeKey]bool)
	var routes []routeKey
	for path, methods := range spec.Paths {
		for method := range methods {
			upper := strings.ToUpper(method)
			switch upper {
			case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS":
				normalized := normalizeSpecPath(path)
				key := routeKey{Method: upper, Path: normalized}
				if !seen[key] {
					seen[key] = true
					routes = append(routes, key)
				}
			}
		}
	}

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