package api

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"testing"
	"time"
)

// A local preview of the admin console with no gateway behind it.
//
//	cd server && ADMIN_PREVIEW=1 go test ./internal/api -run TestAdminPreview -timeout 0
//	→ http://127.0.0.1:7777/admin/overview
//
// It serves the same finished bytes the real console serves — adminRendered, composed from
// the embedded shell, stylesheet and fragments — so what appears here is what a deployment
// would show. What it fakes is only the data: /admin/api/* answers from the canned status
// below, which is what makes it runnable without Postgres, Redis or an Emby to talk to.
//
// It is a test so that it can reach adminRendered, which is unexported for the good reason
// that composing the console is nobody else's business. It skips unless ADMIN_PREVIEW is
// set, so an ordinary `go test ./...` never blocks on a server that runs until interrupted.
func TestAdminPreview(t *testing.T) {
	if os.Getenv("ADMIN_PREVIEW") == "" {
		t.Skip("set ADMIN_PREVIEW=1 to serve the console locally")
	}
	address := os.Getenv("ADMIN_PREVIEW_ADDR")
	if address == "" {
		address = "127.0.0.1:7777"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/admin/api/", func(w http.ResponseWriter, r *http.Request) {
		body, ok := adminPreviewData()[strings.TrimSuffix(r.URL.Path, "/")]
		if !ok {
			body = map[string]any{}
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(body)
	})
	mux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) {
		id := strings.TrimPrefix(r.URL.Path, "/admin/")
		if id == "" {
			id = "overview"
		}
		// A hidden page is addressed by a route carrying something else in the path, so the
		// preview takes the first segment and lets /admin/accounts/42 render the account page.
		id, _, _ = strings.Cut(id, "/")
		page, ok := adminRendered[id]
		if !ok {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Header().Set("Cache-Control", "no-store")
		_, _ = w.Write(page)
	})
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/admin/overview", http.StatusFound)
	})

	fmt.Printf("\nadmin console preview → http://%s/admin/overview  (ctrl-c to stop)\n\n", address)
	if err := http.ListenAndServe(address, mux); err != nil {
		t.Fatal(err)
	}
}

// Enough of a household for every page to draw itself: a library, two viewers, two
// televisions, a run that succeeded and one that did not.
func adminPreviewData() map[string]any {
	now := time.Now().UTC()
	stamp := func(d time.Duration) string { return now.Add(-d).Format(time.RFC3339) }

	// One set of each presence state, because the dot is three-valued and a preview that
	// only ever showed green would not be a preview of it.
	build := func(version string, age time.Duration) map[string]any {
		return map[string]any{"version": version,
			"firstSeen": stamp(age + 24*time.Hour), "lastSeen": stamp(age)}
	}
	clients := []any{
		map[string]any{"deviceId": "tv-1", "deviceName": "Living room", "username": "matt",
			"version": "0.1.74", "lastSeen": stamp(30 * time.Second),
			"capabilities": []string{"server_features_v1", "install_permission_v1"},
			"versions": []any{build("0.1.74", 30*time.Second), build("0.1.72", 9*24*time.Hour),
				build("0.1.68", 40*24*time.Hour)}},
		map[string]any{"deviceId": "tv-2", "deviceName": "Bedroom", "username": "sam",
			"version": "0.1.71", "lastSeen": stamp(2 * 24 * time.Hour), "capabilities": []string{},
			"versions": []any{build("0.1.71", 2*24*time.Hour)}},
		map[string]any{"deviceId": "tv-3", "deviceName": "Spare room", "username": "sam",
			"version": "0.1.62", "lastSeen": stamp(70 * 24 * time.Hour),
			"capabilities": []string{"server_features_v1"},
			"versions":     []any{build("0.1.62", 70*24*time.Hour)}},
	}
	features := []any{
		map[string]any{"key": "subtitle_download", "label": "Download subtitles",
			"description": "Fetch a missing subtitle from the player.", "enabled": true, "source": "default"},
		map[string]any{"key": "install_permission_prompt", "label": "Install permission step",
			"description": "Show the sideloading permission screen during setup.", "enabled": true, "source": "override"},
		map[string]any{"key": "media_requests", "label": "Request it",
			"description": "Let viewers ask for a title the library does not have.", "enabled": false, "source": "default"},
	}
	status := map[string]any{
		"serverVersion": "preview",
		"library": map[string]any{"total": 18422, "lastSynced": stamp(42 * time.Minute),
			"byType": map[string]any{"Movie": 4210, "Series": 612, "Episode": 13600}},
		"requestUsers": []any{"matt", "sam", "alex", "ros"},
		"clients":      clients,
		"features":     map[string]any{"revision": 12, "safeMode": false, "features": features},
		"maintenance":  map[string]any{"enabled": false, "message": ""},
		"updatePolicy": map[string]any{"enabled": true, "latestVersion": "0.1.74",
			"minimumVersion": "", "downloadUrl": "https://example.invalid/memby.apk"},
		"playbackPolicy": map[string]any{"prerollEnabled": true, "prerollDurationMs": 6500},
		"syncRunning":    false,
		"syncEvery":      "1h",
		"radarrReady":    true,
		"sonarrReady":    false,
		"mdblist": map[string]any{"enabled": true, "cachedTitles": 3121, "staleTitles": 44,
			"apiKeyConfigured": true, "sources": []any{
				map[string]any{"key": "imdb", "label": "IMDb", "enabled": true},
				map[string]any{"key": "tmdb", "label": "TMDb", "enabled": true},
				map[string]any{"key": "tomatoes", "label": "Rotten Tomatoes", "enabled": false},
			}},
		"forYou": map[string]any{"candidates": 2400, "profiles": 5, "tracearrSessions": 812,
			"lastFullImport": stamp(30 * time.Hour)},
		"forYouRunning": false,
		"runs": []any{
			map[string]any{"startedAt": stamp(42 * time.Minute), "kind": "incremental",
				"status": "success", "itemsUpserted": 24},
			map[string]any{"startedAt": stamp(102 * time.Minute), "kind": "incremental",
				"status": "running", "itemsUpserted": 0},
			map[string]any{"startedAt": stamp(162 * time.Minute), "kind": "full",
				"status": "failed", "itemsUpserted": 0, "error": "dial tcp: connection refused"},
		},
	}

	return map[string]any{
		"/admin/api/status": status,
		"/admin/api/runtime": map[string]any{"goroutines": 84, "heapInuse": 41943040,
			"sys": 92274688, "numGc": 311, "nextGc": 62914560, "gomaxprocs": 4},
		"/admin/api/accounts": map[string]any{
			"accounts": []any{
				map[string]any{"userId": "u-1", "username": "matt", "onboardingCompleted": true,
					"preferencesRevision": 12, "devices": []any{clients[0]}},
				map[string]any{"userId": "u-2", "username": "sam jones", "onboardingCompleted": false,
					"preferencesRevision": 3, "devices": []any{clients[1], clients[2]}},
			},
			"preferenceCatalogue": []any{},
		},
		"/admin/api/events": map[string]any{"next": 1, "hasMore": false, "dropped": 0, "events": []any{
			map[string]any{"occurredAt": stamp(9 * time.Second), "level": "INFO", "message": "playback started",
				"attributes": map[string]any{"component": "playback", "user": "matt", "device": "Living room",
					"client": "0.1.74", "title": "Arrival"}},
			map[string]any{"occurredAt": stamp(24 * time.Second), "level": "DEBUG", "message": "search",
				"attributes": map[string]any{"component": "search", "user": "sam", "query": "arr"}},
			map[string]any{"occurredAt": stamp(51 * time.Second), "level": "WARN", "message": "emby slow to answer",
				"attributes": map[string]any{"component": "emby", "duration": "4.2s", "path": "/Items"}},
			map[string]any{"occurredAt": stamp(2 * time.Minute), "level": "ERROR", "message": "library sync failed",
				"attributes": map[string]any{"component": "library", "error": "dial tcp: connection refused"}},
		}},
		"/admin/api/engagement": map[string]any{"rows": []any{
			map[string]any{"rowId": "continue", "title": "Continue watching", "impressions": 1840,
				"focuses": 620, "selections": 210, "averageDwellMs": 2400},
			map[string]any{"rowId": "favorites", "title": "Favourites", "impressions": 1610,
				"focuses": 300, "selections": 74, "averageDwellMs": 1800},
		}},
		"/admin/api/journeys": map[string]any{
			"days": 30, "retentionDays": 90,
			"stats": map[string]any{"events": 184, "journeys": 28, "viewers": 2,
				"completed": 21, "abandoned": 5, "active": 2, "averageSteps": 6.6,
				"averageTimeMs": 1140000, "completionRate": 0.81},
			"users": []any{
				map[string]any{"userId": "u-1", "username": "matt", "events": 120, "journeys": 18, "lastActiveAt": stamp(9 * time.Minute)},
				map[string]any{"userId": "u-2", "username": "sam", "events": 64, "journeys": 10, "lastActiveAt": stamp(3 * time.Hour)},
			},
			"features": []any{
				map[string]any{"feature": "playback", "uses": 31, "lastUsedAt": stamp(9 * time.Minute)},
				map[string]any{"feature": "search", "uses": 12, "lastUsedAt": stamp(3 * time.Hour)},
			},
			"actions": []any{
				map[string]any{"category": "content", "action": "open", "events": 44, "journeys": 22},
				map[string]any{"category": "playback", "action": "start", "events": 24, "journeys": 18},
			},
			"paths": []any{
				map[string]any{"from": "home", "to": "details", "count": 31},
				map[string]any{"from": "details", "to": "playback", "count": 18},
			},
		},
		"/admin/api/requests": map[string]any{"requests": []any{}},
		// A prefix among the terms and an unattributed row in the log, because both are
		// ordinary here and a preview showing neither would not be a preview of this page.
		"/admin/api/searches": map[string]any{
			"days": 7, "retentionDays": searchWindowDays,
			"termLimit": searchTermLimit, "eventLimit": searchEventLimit,
			"totals": map[string]any{"searches": 214, "queries": 96, "viewers": 2},
			"terms": []any{
				map[string]any{"query": "severance", "searches": 18, "viewers": 2, "lastAt": stamp(40 * time.Minute)},
				map[string]any{"query": "dune", "searches": 11, "viewers": 1, "lastAt": stamp(3 * time.Hour)},
				map[string]any{"query": "sev", "searches": 9, "viewers": 2, "lastAt": stamp(40 * time.Minute)},
				map[string]any{"query": "the bear", "searches": 4, "viewers": 1, "lastAt": stamp(2 * 24 * time.Hour)},
			},
			"recent": []any{
				map[string]any{"occurredAt": stamp(40 * time.Minute), "userId": "u-1",
					"username": "matt", "query": "severance"},
				map[string]any{"occurredAt": stamp(3 * time.Hour), "userId": "u-2",
					"username": "sam", "query": "dune"},
				map[string]any{"occurredAt": stamp(26 * time.Hour), "userId": "u-9",
					"username": "", "query": "the bear"},
			},
		},
	}
}
