package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"regexp"
	"testing"
	"time"

	tusd "github.com/tus/tusd/pkg/handler"

	"scm.bstein.dev/bstein/Pegasus/backend/internal"
)

func TestSanitizeSegment(t *testing.T) {
	cases := map[string]string{
		"":                  "",
		"  Family Videos  ": "Family_Videos",
		"photos/2026/trip":  "photos",
		"bad#$name":         "bad__name",
		"many   spaces":     "many___spaces",
		"../../etc/passwd":  "..",
	}

	for in, want := range cases {
		if got := sanitizeSegment(in); got != want {
			t.Fatalf("sanitizeSegment(%q)=%q want=%q", in, got, want)
		}
	}
}

func TestContains(t *testing.T) {
	vals := []string{"a", "b", "c"}
	if !contains(vals, "b") {
		t.Fatalf("expected contains to return true")
	}
	if contains(vals, "z") {
		t.Fatalf("expected contains to return false")
	}
}

func TestSanitizeDescriptorAndStemOf(t *testing.T) {
	if got := sanitizeDescriptor("  cool.. file$$$  "); got != "cool_file_" {
		t.Fatalf("unexpected sanitized descriptor %q", got)
	}
	if got := sanitizeDescriptor(""); got != "upload" {
		t.Fatalf("expected upload fallback, got %q", got)
	}
	if got := sanitizeDescriptor("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); len(got) != 64 {
		t.Fatalf("expected truncation to 64 chars, got %d", len(got))
	}
	if got := stemOf("My.Video.File.mp4"); got != "My_Video_File" {
		t.Fatalf("unexpected stem %q", got)
	}
}

func TestComposeFinalName(t *testing.T) {
	got := composeFinalName("2026-04-09", "My Desc", "Cool.Movie.MP4")
	want := "2026.04.09.My_Desc.Cool_Movie.mp4"
	if got != want {
		t.Fatalf("composeFinalName got=%q want=%q", got, want)
	}

	fallback := composeFinalName("not-a-date", "", "file")
	if !regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}\.upload\.file\.bin$`).MatchString(fallback) {
		t.Fatalf("unexpected fallback final name %q", fallback)
	}
}

func TestCorsForTusHandlesPreflight(t *testing.T) {
	h := corsForTus(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatalf("next handler should not run for preflight")
	}))

	req := httptest.NewRequest(http.MethodOptions, "/tus/abc", nil)
	req.Header.Set("Origin", "https://pegasus.bstein.dev")
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusNoContent {
		t.Fatalf("expected 204 for preflight, got %d", rr.Code)
	}
	if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://pegasus.bstein.dev" {
		t.Fatalf("unexpected allow-origin %q", got)
	}
}

func TestCorsForTusPassesThrough(t *testing.T) {
	called := false
	h := corsForTus(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		w.WriteHeader(http.StatusTeapot)
	}))

	req := httptest.NewRequest(http.MethodGet, "/tus/abc", nil)
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)

	if !called {
		t.Fatalf("expected wrapped handler to be called")
	}
	if rr.Code != http.StatusTeapot {
		t.Fatalf("expected wrapped response code, got %d", rr.Code)
	}
}

func TestSessionRequired(t *testing.T) {
	protected := sessionRequired(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	}))

	t.Run("unauthorized without cookie", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/tus/", nil)
		rr := httptest.NewRecorder()
		protected.ServeHTTP(rr, req)
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", rr.Code)
		}
	})

	t.Run("authorized with session cookie", func(t *testing.T) {
		cookieRR := httptest.NewRecorder()
		if err := internal.SetSession(cookieRR, "brad", "jf-token"); err != nil {
			t.Fatalf("SetSession failed: %v", err)
		}
		cookies := cookieRR.Result().Cookies()
		if len(cookies) == 0 {
			t.Fatalf("expected session cookie")
		}

		req := httptest.NewRequest(http.MethodGet, "/tus/", nil)
		req.AddCookie(cookies[0])
		rr := httptest.NewRecorder()
		protected.ServeHTTP(rr, req)
		if rr.Code != http.StatusNoContent {
			t.Fatalf("expected 204, got %d", rr.Code)
		}
	})
}

func TestClaimsFromHook(t *testing.T) {
	cookieRR := httptest.NewRecorder()
	if err := internal.SetSession(cookieRR, "alice", "jf-token"); err != nil {
		t.Fatalf("SetSession failed: %v", err)
	}
	cookies := cookieRR.Result().Cookies()
	if len(cookies) == 0 {
		t.Fatalf("expected session cookie")
	}

	ev := tusd.HookEvent{
		HTTPRequest: tusd.HTTPRequest{Header: http.Header{
			"Cookie": []string{cookies[0].Name + "=" + cookies[0].Value},
		}},
	}
	claims, err := claimsFromHook(ev)
	if err != nil {
		t.Fatalf("claimsFromHook failed: %v", err)
	}
	if claims.Username != "alice" || claims.JFToken != "jf-token" {
		t.Fatalf("unexpected claims %#v", claims)
	}

	_, err = claimsFromHook(tusd.HookEvent{})
	if err == nil {
		t.Fatalf("expected missing-header error")
	}
}

func TestMoveFromTusMovesFileAndCleansSidecars(t *testing.T) {
	origTusDir := tusDir
	tusDir = t.TempDir()
	t.Cleanup(func() {
		tusDir = origTusDir
	})

	origDryRun := internal.DryRun
	internal.DryRun = false
	t.Cleanup(func() {
		internal.DryRun = origDryRun
	})

	id := "upload-id-123"
	src := filepath.Join(tusDir, id+".bin")
	if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
		t.Fatalf("write src failed: %v", err)
	}
	_ = os.WriteFile(filepath.Join(tusDir, id+".info"), []byte("meta"), 0o644)
	_ = os.WriteFile(filepath.Join(tusDir, id+".json"), []byte("meta"), 0o644)

	dst := filepath.Join(t.TempDir(), "final", "file.bin")
	ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}

	if err := moveFromTus(ev, dst); err != nil {
		t.Fatalf("moveFromTus failed: %v", err)
	}

	b, err := os.ReadFile(dst)
	if err != nil {
		t.Fatalf("expected moved file at destination: %v", err)
	}
	if string(b) != "payload" {
		t.Fatalf("unexpected destination payload %q", string(b))
	}
	if _, err := os.Stat(src); !os.IsNotExist(err) {
		t.Fatalf("expected source file to be removed, stat err=%v", err)
	}
	if _, err := os.Stat(filepath.Join(tusDir, id+".info")); !os.IsNotExist(err) {
		t.Fatalf("expected .info sidecar cleanup, stat err=%v", err)
	}
	if _, err := os.Stat(filepath.Join(tusDir, id+".json")); !os.IsNotExist(err) {
		t.Fatalf("expected .json sidecar cleanup, stat err=%v", err)
	}
}

func TestMoveFromTusDryRun(t *testing.T) {
	origTusDir := tusDir
	tusDir = t.TempDir()
	t.Cleanup(func() {
		tusDir = origTusDir
	})

	origDryRun := internal.DryRun
	internal.DryRun = true
	t.Cleanup(func() {
		internal.DryRun = origDryRun
	})

	id := "upload-id-dry"
	src := filepath.Join(tusDir, id)
	if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
		t.Fatalf("write src failed: %v", err)
	}
	dst := filepath.Join(t.TempDir(), "dst.bin")
	ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}

	if err := moveFromTus(ev, dst); err != nil {
		t.Fatalf("moveFromTus dry-run failed: %v", err)
	}
	if _, err := os.Stat(src); err != nil {
		t.Fatalf("expected source file to remain in dry-run mode: %v", err)
	}
	if _, err := os.Stat(dst); !os.IsNotExist(err) {
		t.Fatalf("expected destination to be absent in dry-run mode")
	}
}

func TestMoveFromTusMissingDataFile(t *testing.T) {
	origTusDir := tusDir
	tusDir = t.TempDir()
	t.Cleanup(func() {
		tusDir = origTusDir
	})

	ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: "missing"}}
	err := moveFromTus(ev, filepath.Join(t.TempDir(), "dst.bin"))
	if err == nil {
		t.Fatalf("expected not-found error")
	}
}

func TestMoveFromTusFallbackCopiesWhenRenameFails(t *testing.T) {
	origTusDir := tusDir
	origRenameFile := renameFile
	tusDir = t.TempDir()
	renameFile = func(string, string) error {
		return fmt.Errorf("rename blocked")
	}
	t.Cleanup(func() {
		tusDir = origTusDir
		renameFile = origRenameFile
	})

	origDryRun := internal.DryRun
	internal.DryRun = false
	t.Cleanup(func() {
		internal.DryRun = origDryRun
	})

	id := "upload-id-copy"
	src := filepath.Join(tusDir, id)
	if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
		t.Fatalf("write src failed: %v", err)
	}
	dst := filepath.Join(t.TempDir(), "fallback", "file.bin")
	ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}

	if err := moveFromTus(ev, dst); err != nil {
		t.Fatalf("moveFromTus fallback failed: %v", err)
	}
	b, err := os.ReadFile(dst)
	if err != nil {
		t.Fatalf("read dst failed: %v", err)
	}
	if string(b) != "payload" {
		t.Fatalf("unexpected dst payload %q", string(b))
	}
	if _, err := os.Stat(src); !os.IsNotExist(err) {
		t.Fatalf("expected source to be removed, stat err=%v", err)
	}
}

func TestMoveFromTusScansForPrefixedFiles(t *testing.T) {
	origTusDir := tusDir
	origRenameFile := renameFile
	tusDir = t.TempDir()
	renameFile = func(string, string) error {
		return fmt.Errorf("rename blocked")
	}
	t.Cleanup(func() {
		tusDir = origTusDir
		renameFile = origRenameFile
	})

	origDryRun := internal.DryRun
	internal.DryRun = false
	t.Cleanup(func() {
		internal.DryRun = origDryRun
	})

	id := "pref"
	src := filepath.Join(tusDir, id+"-suffix.bin")
	if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
		t.Fatalf("write src failed: %v", err)
	}
	dst := filepath.Join(t.TempDir(), "scan", "file.bin")
	ev := tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}

	if err := moveFromTus(ev, dst); err != nil {
		t.Fatalf("moveFromTus scan failed: %v", err)
	}
	if _, err := os.Stat(dst); err != nil {
		t.Fatalf("expected scanned destination to exist: %v", err)
	}
}

func TestMoveFromTusOpenAndCreateFailures(t *testing.T) {
	t.Run("open failure", func(t *testing.T) {
		origTusDir := tusDir
		origRenameFile := renameFile
		tusDir = t.TempDir()
		renameFile = func(string, string) error {
			return fmt.Errorf("rename blocked")
		}
		t.Cleanup(func() {
			tusDir = origTusDir
			renameFile = origRenameFile
		})

		id := "open-failure"
		src := filepath.Join(tusDir, id)
		if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
			t.Fatalf("write src failed: %v", err)
		}
		renameFile = func(string, string) error {
			_ = os.Remove(src)
			return fmt.Errorf("rename blocked")
		}

		err := moveFromTus(tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}, filepath.Join(t.TempDir(), "dst.bin"))
		if err == nil {
			t.Fatalf("expected open failure")
		}
	})

	t.Run("create failure", func(t *testing.T) {
		origTusDir := tusDir
		origRenameFile := renameFile
		tusDir = t.TempDir()
		renameFile = func(string, string) error {
			return fmt.Errorf("rename blocked")
		}
		t.Cleanup(func() {
			tusDir = origTusDir
			renameFile = origRenameFile
		})

		id := "create-failure"
		src := filepath.Join(tusDir, id)
		if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
			t.Fatalf("write src failed: %v", err)
		}
		dst := filepath.Join(t.TempDir(), "blocked-dst.bin")
		if err := os.MkdirAll(dst, 0o755); err != nil {
			t.Fatalf("mkdir dst blocker failed: %v", err)
		}

		err := moveFromTus(tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}, dst)
		if err == nil {
			t.Fatalf("expected create failure")
		}
	})

	t.Run("mkdir failure", func(t *testing.T) {
		origTusDir := tusDir
		origRenameFile := renameFile
		tusDir = t.TempDir()
		renameFile = func(string, string) error {
			return fmt.Errorf("rename blocked")
		}
		t.Cleanup(func() {
			tusDir = origTusDir
			renameFile = origRenameFile
		})

		id := "mkdir-failure"
		src := filepath.Join(tusDir, id)
		if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
			t.Fatalf("write src failed: %v", err)
		}
		blocker := filepath.Join(t.TempDir(), "blocker")
		if err := os.WriteFile(blocker, []byte("block"), 0o644); err != nil {
			t.Fatalf("write blocker failed: %v", err)
		}
		dst := filepath.Join(blocker, "child.bin")

		err := moveFromTus(tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}, dst)
		if err == nil {
			t.Fatalf("expected mkdir failure")
		}
	})

	t.Run("io copy failure", func(t *testing.T) {
		origTusDir := tusDir
		origRenameFile := renameFile
		tusDir = t.TempDir()
		renameFile = func(src, _ string) error {
			if err := os.Remove(src); err != nil {
				return err
			}
			if err := os.Mkdir(src, 0o755); err != nil {
				return err
			}
			return fmt.Errorf("rename blocked")
		}
		t.Cleanup(func() {
			tusDir = origTusDir
			renameFile = origRenameFile
		})

		id := "copy-failure"
		src := filepath.Join(tusDir, id)
		if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil {
			t.Fatalf("write src failed: %v", err)
		}
		dst := filepath.Join(t.TempDir(), "copy-failure.bin")
		err := moveFromTus(tusd.HookEvent{Upload: tusd.FileInfo{ID: id}}, dst)
		if err == nil {
			t.Fatalf("expected copy failure")
		}
	})
}

func TestWriteJSON(t *testing.T) {
	rr := httptest.NewRecorder()
	writeJSON(rr, map[string]any{"ok": true})
	if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
		t.Fatalf("unexpected content-type %q", ct)
	}
	if rr.Code != http.StatusOK {
		t.Fatalf("unexpected status %d", rr.Code)
	}
	body, _ := io.ReadAll(rr.Result().Body)
	if !regexp.MustCompile(`"ok"\s*:\s*true`).Match(body) {
		t.Fatalf("unexpected JSON body %q", string(body))
	}
}

func TestEnvHelper(t *testing.T) {
	t.Setenv("PEGASUS_TEST_ENV", "value")
	if got := env("PEGASUS_TEST_ENV", "fallback"); got != "value" {
		t.Fatalf("unexpected env result %q", got)
	}
	if got := env("PEGASUS_TEST_ENV_MISSING", "fallback"); got != "fallback" {
		t.Fatalf("unexpected fallback env result %q", got)
	}
}

func TestLoggingRWWriteHeader(t *testing.T) {
	rr := httptest.NewRecorder()
	lw := &loggingRW{ResponseWriter: rr, status: http.StatusOK}
	lw.WriteHeader(http.StatusCreated)
	if lw.status != http.StatusCreated {
		t.Fatalf("expected tracked status %d, got %d", http.StatusCreated, lw.status)
	}
	if rr.Code != http.StatusCreated {
		t.Fatalf("expected recorder status %d, got %d", http.StatusCreated, rr.Code)
	}
}

func TestComposeFinalNameDateFallbackNotEmpty(t *testing.T) {
	got := composeFinalName("", "clip", "v.mkv")
	today := time.Now().Format("2006.01.02")
	if got[:10] != today {
		t.Fatalf("expected date prefix %s got %s", today, got[:10])
	}
}
