package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

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

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

type fakeJellyfin struct {
	mu            sync.Mutex
	authResult    internal.AuthResult
	authErr       error
	refreshTokens []string
}

func (f *fakeJellyfin) AuthenticateByName(username, password string) (internal.AuthResult, error) {
	return f.authResult, f.authErr
}

func (f *fakeJellyfin) RefreshLibrary(userToken string) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.refreshTokens = append(f.refreshTokens, userToken)
}

func (f *fakeJellyfin) refreshCount() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return len(f.refreshTokens)
}

func testUserMap(t *testing.T, payload string) *internal.UserMap {
	t.Helper()
	path := filepath.Join(t.TempDir(), "user-map.yaml")
	if err := os.WriteFile(path, []byte(payload), 0o644); err != nil {
		t.Fatalf("write user map: %v", err)
	}
	um, err := internal.LoadUserMap(path)
	if err != nil {
		t.Fatalf("load user map: %v", err)
	}
	return um
}

func sessionCookie(t *testing.T, username, token string) string {
	t.Helper()
	rr := httptest.NewRecorder()
	if err := internal.SetSession(rr, username, token); err != nil {
		t.Fatalf("SetSession: %v", err)
	}
	cookies := rr.Result().Cookies()
	if len(cookies) == 0 {
		t.Fatalf("expected session cookie")
	}
	return cookies[0].Name + "=" + cookies[0].Value
}

func requestWithCookie(method, target, cookie string, body []byte) *http.Request {
	req := httptest.NewRequest(method, target, bytes.NewReader(body))
	if cookie != "" {
		req.Header.Set("Cookie", cookie)
	}
	return req
}

func TestRouterLifecycleAndFilesystemRoutes(t *testing.T) {
	origMediaRoot := mediaRoot
	origTusDir := tusDir
	origDebug := internal.Debug
	origDryRun := internal.DryRun
	defer func() {
		mediaRoot = origMediaRoot
		tusDir = origTusDir
		internal.Debug = origDebug
		internal.DryRun = origDryRun
	}()

	root := t.TempDir()
	mediaRoot = filepath.Join(root, "media")
	tusDir = filepath.Join(root, "tus")
	internal.Debug = false
	internal.DryRun = false
	if err := os.MkdirAll(filepath.Join(mediaRoot, "library", "album"), 0o2775); err != nil {
		t.Fatalf("mkdir media root: %v", err)
	}
	if err := os.WriteFile(filepath.Join(mediaRoot, "library", "old.txt"), []byte("old"), 0o644); err != nil {
		t.Fatalf("write source file: %v", err)
	}

	um := testUserMap(t, "map:\n  brad: library\n")
	jf := &fakeJellyfin{
		authResult: internal.AuthResult{
			AccessToken: "jf-token",
		},
	}
	jf.authResult.User.Name = "brad"

	router := buildRouter(um, jf)

	t.Run("health and version", func(t *testing.T) {
		rr := httptest.NewRecorder()
		router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/healthz", nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected health status %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/version", nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected version status %d", rr.Code)
		}
	})

	t.Run("login whoami logout", func(t *testing.T) {
		loginBody, _ := json.Marshal(map[string]string{"username": "brad", "password": "pass"})
		rr := httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/login", "", loginBody))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected login status %d: %s", rr.Code, rr.Body.String())
		}
		cookies := rr.Result().Cookies()
		if len(cookies) == 0 {
			t.Fatalf("expected login cookie")
		}
		session := cookies[0].Name + "=" + cookies[0].Value

		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/api/whoami", session, nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected whoami status %d", rr.Code)
		}
		var profile map[string]any
		if err := json.Unmarshal(rr.Body.Bytes(), &profile); err != nil {
			t.Fatalf("decode whoami: %v", err)
		}
		if profile["username"] != "brad" {
			t.Fatalf("unexpected whoami payload %#v", profile)
		}

		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/logout", session, nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected logout status %d", rr.Code)
		}
	})

	session := sessionCookie(t, "brad", "jf-token")

	t.Run("list mkdir rename delete", func(t *testing.T) {
		rr := httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/api/list?lib=library", session, nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected list status %d: %s", rr.Code, rr.Body.String())
		}

		mkdirBody, _ := json.Marshal(map[string]string{"lib": "library", "path": "new-folder"})
		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/mkdir", session, mkdirBody))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected mkdir status %d: %s", rr.Code, rr.Body.String())
		}
		if _, err := os.Stat(filepath.Join(mediaRoot, "library", "new-folder")); err != nil {
			t.Fatalf("expected new folder to exist: %v", err)
		}

		renameBody, _ := json.Marshal(map[string]string{
			"lib":  "library",
			"from": "old.txt",
			"to":   "2026.01.02.Note.old.txt",
		})
		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodPost, "/api/rename", session, renameBody))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected rename status %d: %s", rr.Code, rr.Body.String())
		}
		if _, err := os.Stat(filepath.Join(mediaRoot, "library", "2026.01.02.Note.old.txt")); err != nil {
			t.Fatalf("expected renamed file to exist: %v", err)
		}

		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, requestWithCookie(http.MethodDelete, "/api/file?lib=library&path=new-folder&recursive=true", session, nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected delete status %d: %s", rr.Code, rr.Body.String())
		}
		if _, err := os.Stat(filepath.Join(mediaRoot, "library", "new-folder")); !os.IsNotExist(err) {
			t.Fatalf("expected deleted folder to be absent, err=%v", err)
		}
	})

	t.Run("static metrics and tus auth", func(t *testing.T) {
		rr := httptest.NewRecorder()
		router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected static status %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/metrics", nil))
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected metrics status %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		router.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/tus/123", nil))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected tus auth to reject unauthenticated request, got %d", rr.Code)
		}
	})

	if jf.refreshCount() == 0 {
		t.Fatalf("expected at least one Jellyfin refresh call")
	}
}

func TestDebugRoutesAndUploadProcessing(t *testing.T) {
	origMediaRoot := mediaRoot
	origTusDir := tusDir
	origDebug := internal.Debug
	origDryRun := internal.DryRun
	defer func() {
		mediaRoot = origMediaRoot
		tusDir = origTusDir
		internal.Debug = origDebug
		internal.DryRun = origDryRun
	}()

	root := t.TempDir()
	mediaRoot = filepath.Join(root, "media")
	tusDir = filepath.Join(root, "tus")
	internal.Debug = true
	internal.DryRun = false
	if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil {
		t.Fatalf("mkdir media root: %v", err)
	}

	um := testUserMap(t, "map:\n  brad: library\n")
	jf := &fakeJellyfin{
		authResult: internal.AuthResult{AccessToken: "jf-token"},
	}
	jf.authResult.User.Name = "brad"

	router := buildRouter(um, jf)
	session := sessionCookie(t, "brad", "jf-token")

	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/debug/env", session, nil))
	if rr.Code != http.StatusOK {
		t.Fatalf("unexpected debug env status %d", rr.Code)
	}

	rr = httptest.NewRecorder()
	router.ServeHTTP(rr, requestWithCookie(http.MethodGet, "/debug/write-test", session, nil))
	if rr.Code != http.StatusOK {
		t.Fatalf("unexpected debug write-test status %d", rr.Code)
	}

	srcID := "upload-id-123"
	if err := os.MkdirAll(tusDir, 0o2775); err != nil {
		t.Fatalf("mkdir tus dir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(tusDir, srcID+".bin"), []byte("payload"), 0o644); err != nil {
		t.Fatalf("write tus payload: %v", err)
	}

	event := handler.HookEvent{
		HTTPRequest: handler.HTTPRequest{
			Header: http.Header{"Cookie": []string{session}},
		},
		Upload: handler.FileInfo{
			ID: srcID,
			MetaData: map[string]string{
				"filename": "video.mp4",
				"desc":     "Trip",
				"date":     "2026-01-02",
				"lib":      "library",
			},
		},
	}

	done := make(chan struct{})
	ch := make(chan handler.HookEvent, 2)
	go func() {
		watchUploadCompletions(ch, um, jf)
		close(done)
	}()
	ch <- event
	ch <- handler.HookEvent{
		HTTPRequest: handler.HTTPRequest{
			Header: http.Header{"Cookie": []string{session}},
		},
		Upload: handler.FileInfo{
			ID: "bad-id",
			MetaData: map[string]string{
				"filename": "clip.mp4",
				"lib":      "library",
			},
		},
	}
	close(ch)

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatalf("watchUploadCompletions did not drain channel")
	}

	expected := filepath.Join(mediaRoot, "library", composeFinalName("2026-01-02", "Trip", "video.mp4"))
	if _, err := os.Stat(expected); err != nil {
		t.Fatalf("expected uploaded file at destination: %v", err)
	}
	if jf.refreshCount() == 0 {
		t.Fatalf("expected upload completion to refresh Jellyfin")
	}
}

func TestProcessCompletedUploadErrorPaths(t *testing.T) {
	origMediaRoot := mediaRoot
	origTusDir := tusDir
	origDryRun := internal.DryRun
	defer func() {
		mediaRoot = origMediaRoot
		tusDir = origTusDir
		internal.DryRun = origDryRun
	}()

	root := t.TempDir()
	mediaRoot = filepath.Join(root, "media")
	tusDir = filepath.Join(root, "tus")
	internal.DryRun = false

	um := testUserMap(t, "map:\n  brad: library\n")
	jf := &fakeJellyfin{authResult: internal.AuthResult{AccessToken: "jf-token"}}
	jf.authResult.User.Name = "brad"
	session := sessionCookie(t, "brad", "jf-token")

	t.Run("missing session", func(t *testing.T) {
		ev := handler.HookEvent{}
		if err := processCompletedUpload(ev, um, jf); err == nil {
			t.Fatalf("expected missing session error")
		}
	})

	t.Run("missing mapping", func(t *testing.T) {
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{sessionCookie(t, "nobody", "token")}}},
			Upload: handler.FileInfo{
				ID: "missing-mapping",
				MetaData: map[string]string{
					"filename": "clip.mp4",
					"desc":     "Trip",
					"lib":      "library",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err == nil {
			t.Fatalf("expected missing mapping error")
		}
	})

	t.Run("missing library root isolated", func(t *testing.T) {
		origMediaRoot := mediaRoot
		origTusDir := tusDir
		defer func() {
			mediaRoot = origMediaRoot
			tusDir = origTusDir
		}()

		mediaRoot = filepath.Join(t.TempDir(), "media")
		tusDir = filepath.Join(t.TempDir(), "tus")
		if err := os.MkdirAll(tusDir, 0o2775); err != nil {
			t.Fatalf("mkdir tus dir: %v", err)
		}
		id := "missing-root-isolated"
		if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil {
			t.Fatalf("write tus payload: %v", err)
		}
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}},
			Upload: handler.FileInfo{
				ID: id,
				MetaData: map[string]string{
					"filename": "clip.mp4",
					"desc":     "Trip",
					"lib":      "library",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err == nil {
			t.Fatalf("expected missing library root error")
		}
	})

	t.Run("missing description for video", func(t *testing.T) {
		if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil {
			t.Fatalf("mkdir media root: %v", err)
		}
		id := "video-no-desc"
		if err := os.MkdirAll(tusDir, 0o2775); err != nil {
			t.Fatalf("mkdir tus dir: %v", err)
		}
		if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil {
			t.Fatalf("write tus payload: %v", err)
		}
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}},
			Upload: handler.FileInfo{
				ID: id,
				MetaData: map[string]string{
					"filename": "video.mp4",
					"lib":      "library",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err == nil {
			t.Fatalf("expected missing desc error")
		}
	})

	t.Run("missing library root", func(t *testing.T) {
		id := "missing-root"
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}},
			Upload: handler.FileInfo{
				ID: id,
				MetaData: map[string]string{
					"filename": "clip.mp4",
					"desc":     "Trip",
					"lib":      "library",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err == nil {
			t.Fatalf("expected missing root error")
		}
	})

	t.Run("subdir mkdir failure", func(t *testing.T) {
		if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil {
			t.Fatalf("mkdir media root: %v", err)
		}
		blocker := filepath.Join(mediaRoot, "library", "bad")
		if err := os.WriteFile(blocker, []byte("file"), 0o644); err != nil {
			t.Fatalf("write blocker file: %v", err)
		}
		id := "subdir-failure"
		if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil {
			t.Fatalf("write tus payload: %v", err)
		}
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}},
			Upload: handler.FileInfo{
				ID: id,
				MetaData: map[string]string{
					"filename": "clip.mp4",
					"desc":     "Trip",
					"lib":      "library",
					"subdir":   "bad/child",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err == nil {
			t.Fatalf("expected mkdir failure")
		}
	})

	t.Run("non-video defaults description", func(t *testing.T) {
		if err := os.MkdirAll(filepath.Join(mediaRoot, "library"), 0o2775); err != nil {
			t.Fatalf("mkdir media root: %v", err)
		}
		id := "image-default-desc"
		if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil {
			t.Fatalf("write tus payload: %v", err)
		}
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}},
			Upload: handler.FileInfo{
				ID: id,
				MetaData: map[string]string{
					"filename": "photo.jpg",
					"lib":      "library",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err != nil {
			t.Fatalf("expected default-desc upload to succeed: %v", err)
		}
		expected := filepath.Join(mediaRoot, "library", composeFinalName("", "upload", "photo.jpg"))
		if _, err := os.Stat(expected); err != nil {
			t.Fatalf("expected defaulted upload output: %v", err)
		}
	})

	t.Run("missing filename falls back to upload.bin", func(t *testing.T) {
		id := "missing-filename"
		if err := os.WriteFile(filepath.Join(tusDir, id+".bin"), []byte("payload"), 0o644); err != nil {
			t.Fatalf("write tus payload: %v", err)
		}
		ev := handler.HookEvent{
			HTTPRequest: handler.HTTPRequest{Header: http.Header{"Cookie": []string{session}}},
			Upload: handler.FileInfo{
				ID: id,
				MetaData: map[string]string{
					"desc": "Trip",
					"lib":  "library",
				},
			},
		}
		if err := processCompletedUpload(ev, um, jf); err != nil {
			t.Fatalf("expected upload with fallback filename to succeed: %v", err)
		}
		expected := filepath.Join(mediaRoot, "library", composeFinalName("", "Trip", "upload.bin"))
		if _, err := os.Stat(expected); err != nil {
			t.Fatalf("expected fallback filename output: %v", err)
		}
	})
}
