package sources

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"ymhut-box/server/unified-management/internal/config"
	"ymhut-box/server/unified-management/internal/db"
)

func TestCheckOneTreatsRedirectToOKAsRedirected(t *testing.T) {
	target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("ok"))
	}))
	defer target.Close()
	redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, target.URL, http.StatusFound)
	}))
	defer redirector.Close()

	cfg, store := testStore(t)
	service := NewService(cfg, store)
	item, err := store.UpsertSource(db.Source{
		CategoryID:       "test",
		CategoryName:     "Test",
		SourceID:         "redirect",
		Name:             "Redirect",
		Method:           "GET",
		APIURL:           redirector.URL,
		TimeoutMS:        3000,
		CheckIntervalSec: 300,
		Enabled:          true,
		ClientVisible:    true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := service.CheckOne(context.Background(), item); err != nil {
		t.Fatal(err)
	}
	checked, err := store.GetSourceBySourceID("redirect")
	if err != nil {
		t.Fatal(err)
	}
	if checked.LastStatus != "redirected" {
		t.Fatalf("LastStatus = %q, want redirected", checked.LastStatus)
	}
	if !strings.Contains(checked.LastError, `"redirected":true`) {
		t.Fatalf("LastError does not contain redirect metadata: %s", checked.LastError)
	}
	if checked.ConsecutiveFailure != 0 {
		t.Fatalf("ConsecutiveFailure = %d, want 0", checked.ConsecutiveFailure)
	}
}

func TestQueueCheckAllUsesBackgroundContext(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("ok"))
	}))
	defer server.Close()
	cfg, store := testStore(t)
	service := NewService(cfg, store)
	if _, err := store.UpsertSource(db.Source{
		CategoryID:       "test",
		CategoryName:     "Test",
		SourceID:         "slow-ok",
		Name:             "Slow OK",
		Method:           "GET",
		APIURL:           server.URL,
		TimeoutMS:        1000,
		CheckIntervalSec: 300,
		Enabled:          true,
		ClientVisible:    true,
	}); err != nil {
		t.Fatal(err)
	}
	job := service.QueueCheckAll()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		current, ok := service.CheckJob(job.ID)
		if ok && current.Status == "completed" {
			if current.Stats["ok"] != 1 {
				t.Fatalf("stats = %#v, want one ok", current.Stats)
			}
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatalf("job did not complete: %#v", job)
}

func TestSubscribeEventsBroadcastsToAllSubscribers(t *testing.T) {
	cfg, store := testStore(t)
	service := NewService(cfg, store)
	eventsA, unsubscribeA := service.SubscribeEvents()
	defer unsubscribeA()
	eventsB, unsubscribeB := service.SubscribeEvents()
	defer unsubscribeB()

	service.emit("source_check.completed", map[string]any{"jobId": "demo"})

	assertEvent := func(name string, events <-chan Event) {
		t.Helper()
		select {
		case event := <-events:
			if event.Type != "source_check.completed" || event.Data["jobId"] != "demo" {
				t.Fatalf("%s received unexpected event: %#v", name, event)
			}
		case <-time.After(time.Second):
			t.Fatalf("%s did not receive broadcast event", name)
		}
	}
	assertEvent("subscriber A", eventsA)
	assertEvent("subscriber B", eventsB)
}

func TestCheckOneResolvesNestedJSONMediaURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]any{
			"ok": true,
			"data": map[string]any{
				"ignored": "https://example.test/readme.txt",
				"items": []map[string]any{
					{"name": "first"},
					{"cover": "/media/poster.webp"},
				},
			},
		})
	}))
	defer server.Close()
	cfg, store := testStore(t)
	service := NewService(cfg, store)
	item, err := store.UpsertSource(db.Source{
		CategoryID:       "image",
		CategoryName:     "Image",
		SourceID:         "json-cover",
		Name:             "JSON Cover",
		Method:           "GET",
		APIURL:           server.URL + "/api/random",
		TimeoutMS:        3000,
		CheckIntervalSec: 300,
		Enabled:          true,
		ClientVisible:    true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := service.CheckOne(context.Background(), item); err != nil {
		t.Fatal(err)
	}
	checked, err := store.GetSourceBySourceID("json-cover")
	if err != nil {
		t.Fatal(err)
	}
	meta := parseHealthMeta(checked.LastError)
	if meta["resolvedUrl"] != server.URL+"/media/poster.webp" {
		t.Fatalf("resolvedUrl = %#v, want relative media URL", meta["resolvedUrl"])
	}
	if meta["resolvedKey"] != "data.items.cover" {
		t.Fatalf("resolvedKey = %#v", meta["resolvedKey"])
	}
	if meta["mediaType"] != "image" {
		t.Fatalf("mediaType = %#v, want image", meta["mediaType"])
	}
	catalog, err := service.Catalog(false)
	if err != nil {
		t.Fatal(err)
	}
	categories := catalog["categories"].([]map[string]any)
	sub := categories[0]["subcategories"].([]map[string]any)[0]
	if sub["resolvedUrl"] != server.URL+"/media/poster.webp" {
		t.Fatalf("catalog resolvedUrl = %#v", sub["resolvedUrl"])
	}
	endpoints, err := service.Endpoints(false)
	if err != nil {
		t.Fatal(err)
	}
	if endpoints[0]["resolvedUrl"] != server.URL+"/media/poster.webp" {
		t.Fatalf("endpoint resolvedUrl = %#v", endpoints[0]["resolvedUrl"])
	}
	if endpoints[0]["urlTemplate"] != server.URL+"/api/random" {
		t.Fatalf("urlTemplate changed: %#v", endpoints[0]["urlTemplate"])
	}
}

func TestCheckOneResolvesTextMediaURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		_, _ = w.Write([]byte(`play: https://cdn.example.test/video/sample.mp4`))
	}))
	defer server.Close()
	cfg, store := testStore(t)
	service := NewService(cfg, store)
	item, err := store.UpsertSource(db.Source{
		CategoryID:       "video",
		CategoryName:     "Video",
		SourceID:         "text-video",
		Name:             "Text Video",
		Method:           "GET",
		APIURL:           server.URL,
		TimeoutMS:        3000,
		CheckIntervalSec: 300,
		Enabled:          true,
		ClientVisible:    true,
	})
	if err != nil {
		t.Fatal(err)
	}
	if err := service.CheckOne(context.Background(), item); err != nil {
		t.Fatal(err)
	}
	checked, err := store.GetSourceBySourceID("text-video")
	if err != nil {
		t.Fatal(err)
	}
	meta := parseHealthMeta(checked.LastError)
	if meta["resolvedUrl"] != "https://cdn.example.test/video/sample.mp4" {
		t.Fatalf("resolvedUrl = %#v", meta["resolvedUrl"])
	}
	if meta["mediaType"] != "video" {
		t.Fatalf("mediaType = %#v, want video", meta["mediaType"])
	}
}

func testStore(t *testing.T) (*config.Config, *db.Store) {
	t.Helper()
	dir := t.TempDir()
	cfg := &config.Config{
		BaseDir:         dir,
		StorageDir:      filepath.Join(dir, "storage"),
		DataDir:         filepath.Join(dir, "data"),
		UpdatePublicDir: filepath.Join(dir, "data", "update", "public"),
		DownloadsDir:    filepath.Join(dir, "data", "update", "public", "downloads"),
		BaseURL:         "https://update.ymhut.cn",
		Database: config.DatabaseConfig{
			Provider:          "sqlite",
			SQLitePath:        filepath.Join(dir, "storage", "unified.sqlite"),
			HealthIntervalSec: 30,
		},
	}
	store, err := db.Open(cfg)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { store.Close() })
	return cfg, store
}
