package main

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

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

func TestResolveLibraryRootFallback(t *testing.T) {
	um := &internal.UserMap{Map: map[string]internal.StringOrList{"brad": {"library", "alt"}}}
	got, err := resolveLibraryRoot(um, "brad", "not-allowed")
	if err != nil {
		t.Fatalf("resolveLibraryRoot failed: %v", err)
	}
	if got != "library" {
		t.Fatalf("expected default root, got %q", got)
	}
}

func TestListHandlerErrorPaths(t *testing.T) {
	origMediaRoot := mediaRoot
	defer func() { mediaRoot = origMediaRoot }()
	mediaRoot = t.TempDir()

	um := &internal.UserMap{Map: map[string]internal.StringOrList{"brad": {"library"}}}
	handler := listHandler(um)

	t.Run("unauthorized", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/api/list", nil))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected unauthorized status, got %d", rr.Code)
		}
	})

	t.Run("forbidden escape", func(t *testing.T) {
		cookie := sessionCookie(t, "brad", "token")
		req := httptest.NewRequest(http.MethodGet, "/api/list?path=../secret", nil)
		req.Header.Set("Cookie", cookie)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden status, got %d", rr.Code)
		}
	})
}

func TestRenameDeleteMkdirErrorPaths(t *testing.T) {
	origMediaRoot := mediaRoot
	origDryRun := internal.DryRun
	defer func() {
		mediaRoot = origMediaRoot
		internal.DryRun = origDryRun
	}()
	mediaRoot = t.TempDir()
	internal.DryRun = true

	um := &internal.UserMap{Map: map[string]internal.StringOrList{"brad": {"library"}}}
	jf := &fakeJellyfin{}
	cookie := sessionCookie(t, "brad", "token")

	t.Run("rename bad json", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodPost, "/api/rename", bytes.NewBufferString("{bad"))
		req.Header.Set("Cookie", cookie)
		renameHandler(um, jf).ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected bad request, got %d", rr.Code)
		}
	})

	t.Run("rename invalid name", func(t *testing.T) {
		body := []byte(`{"lib":"library","from":"old.txt","to":"bad-name.txt"}`)
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodPost, "/api/rename", bytes.NewReader(body))
		req.Header.Set("Cookie", cookie)
		renameHandler(um, jf).ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected bad request, got %d", rr.Code)
		}
	})

	t.Run("mkdir bad json and empty folder", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString("{bad"))
		req.Header.Set("Cookie", cookie)
		mkdirHandler(um, jf).ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected bad request, got %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		req = httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString(`{"lib":"library","path":"   "}`))
		req.Header.Set("Cookie", cookie)
		mkdirHandler(um, jf).ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected bad request, got %d", rr.Code)
		}
	})

	t.Run("delete unauthorized", func(t *testing.T) {
		rr := httptest.NewRecorder()
		deleteHandler(um, jf).ServeHTTP(rr, httptest.NewRequest(http.MethodDelete, "/api/file?path=a", nil))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected unauthorized, got %d", rr.Code)
		}
	})
}

func TestDebugHandlersDisabled(t *testing.T) {
	origDebug := internal.Debug
	defer func() { internal.Debug = origDebug }()
	internal.Debug = false

	rr := httptest.NewRecorder()
	debugEnvHandler().ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/debug/env", nil))
	if rr.Code != http.StatusForbidden {
		t.Fatalf("expected forbidden when debug disabled, got %d", rr.Code)
	}

	rr = httptest.NewRecorder()
	debugWriteTestHandler(&internal.UserMap{Map: map[string]internal.StringOrList{"brad": {"library"}}}).ServeHTTP(
		rr,
		httptest.NewRequest(http.MethodGet, "/debug/write-test", nil),
	)
	if rr.Code != http.StatusForbidden {
		t.Fatalf("expected forbidden when debug disabled, got %d", rr.Code)
	}
}

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

	root := t.TempDir()
	mediaRoot = root
	internal.DryRun = false
	internal.Debug = true

	um := &internal.UserMap{Map: map[string]internal.StringOrList{"brad": {"library"}}}
	jf := &fakeJellyfin{}
	cookie := sessionCookie(t, "brad", "token")
	handlerList := listHandler(um)
	handlerRename := renameHandler(um, jf)
	handlerDelete := deleteHandler(um, jf)
	handlerMkdir := mkdirHandler(um, jf)

	if err := os.MkdirAll(filepath.Join(root, "library", "album"), 0o2775); err != nil {
		t.Fatalf("mkdir library: %v", err)
	}
	if err := os.MkdirAll(filepath.Join(root, "library", "zeta"), 0o2775); err != nil {
		t.Fatalf("mkdir zeta: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "library", "keep.txt"), []byte("keep"), 0o644); err != nil {
		t.Fatalf("write keep file: %v", err)
	}

	t.Run("list same-type sorting", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, "/api/list?lib=library", nil)
		req.Header.Set("Cookie", cookie)
		handlerList.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected list status %d: %s", rr.Code, rr.Body.String())
		}
	})

	t.Run("list nested path", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, "/api/list?lib=library&path=album", nil)
		req.Header.Set("Cookie", cookie)
		handlerList.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("unexpected list status %d: %s", rr.Code, rr.Body.String())
		}
	})

	t.Run("list missing directory", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, "/api/list?lib=library&path=missing", nil)
		req.Header.Set("Cookie", cookie)
		handlerList.ServeHTTP(rr, req)
		if rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected internal error, got %d", rr.Code)
		}
	})

	t.Run("list missing mapping", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, "/api/list?lib=library", nil)
		req.Header.Set("Cookie", sessionCookie(t, "nobody", "token"))
		handlerList.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden for missing mapping, got %d", rr.Code)
		}
	})

	t.Run("rename unauthorized and escape", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handlerRename.ServeHTTP(rr, httptest.NewRequest(http.MethodPost, "/api/rename", bytes.NewBufferString(`{"lib":"library","from":"old.txt","to":"2026.01.02.Note.old.txt"}`)))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected unauthorized rename, got %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		body := bytes.NewBufferString(`{"lib":"library","from":"../escape.txt","to":"2026.01.02.Note.escape.txt"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/rename", body)
		req.Header.Set("Cookie", cookie)
		handlerRename.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden && rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected from-path escape or missing-source failure, got %d", rr.Code)
		}
	})

	t.Run("rename forbidden root and dry run", func(t *testing.T) {
		rr := httptest.NewRecorder()
		body := bytes.NewBufferString(`{"lib":"library","from":"old.txt","to":"2026.01.02.Note.old.txt"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/rename", body)
		req.Header.Set("Cookie", sessionCookie(t, "nobody", "token"))
		handlerRename.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden rename root, got %d", rr.Code)
		}

		orig := internal.DryRun
		internal.DryRun = true
		defer func() { internal.DryRun = orig }()
		rr = httptest.NewRecorder()
		body = bytes.NewBufferString(`{"lib":"library","from":"old.txt","to":"2026.01.02.Note.old.txt"}`)
		req = httptest.NewRequest(http.MethodPost, "/api/rename", body)
		req.Header.Set("Cookie", cookie)
		handlerRename.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("expected dry-run rename success, got %d", rr.Code)
		}
	})

	t.Run("rename to-path escape", func(t *testing.T) {
		orig := internal.DryRun
		internal.DryRun = false
		defer func() { internal.DryRun = orig }()
		rr := httptest.NewRecorder()
		body := bytes.NewBufferString(`{"lib":"library","from":"old.txt","to":"../2026.01.02.Note.escape.txt"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/rename", body)
		req.Header.Set("Cookie", cookie)
		handlerRename.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden rename escape, got %d", rr.Code)
		}
	})

	t.Run("rename missing source", func(t *testing.T) {
		rr := httptest.NewRecorder()
		body := bytes.NewBufferString(`{"lib":"library","from":"missing.txt","to":"2026.01.02.Note.missing.txt"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/rename", body)
		req.Header.Set("Cookie", cookie)
		handlerRename.ServeHTTP(rr, req)
		if rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected rename error, got %d", rr.Code)
		}
	})

	t.Run("delete unauthorized and escape", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handlerDelete.ServeHTTP(rr, httptest.NewRequest(http.MethodDelete, "/api/file?path=a", nil))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected unauthorized delete, got %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodDelete, "/api/file?lib=library&path=../escape", nil)
		req.Header.Set("Cookie", cookie)
		handlerDelete.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden && rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected delete escape failure, got %d", rr.Code)
		}
	})

	t.Run("delete forbidden root and missing file", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodDelete, "/api/file?lib=library&path=keep.txt", nil)
		req.Header.Set("Cookie", sessionCookie(t, "nobody", "token"))
		handlerDelete.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden delete root, got %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		req = httptest.NewRequest(http.MethodDelete, "/api/file?lib=library&path=missing.txt", nil)
		req.Header.Set("Cookie", cookie)
		handlerDelete.ServeHTTP(rr, req)
		if rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected missing-file delete error, got %d", rr.Code)
		}
	})

	t.Run("delete file without recursion", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodDelete, "/api/file?lib=library&path=keep.txt", nil)
		req.Header.Set("Cookie", cookie)
		handlerDelete.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("expected delete success, got %d", rr.Code)
		}
		if _, err := os.Stat(filepath.Join(root, "library", "keep.txt")); !os.IsNotExist(err) {
			t.Fatalf("expected keep.txt to be removed")
		}
	})

	t.Run("delete dry run", func(t *testing.T) {
		orig := internal.DryRun
		internal.DryRun = true
		defer func() { internal.DryRun = orig }()
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodDelete, "/api/file?lib=library&path=keep.txt", nil)
		req.Header.Set("Cookie", cookie)
		handlerDelete.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("expected dry-run delete success, got %d", rr.Code)
		}
	})

	t.Run("mkdir unauthorized and path escape", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handlerMkdir.ServeHTTP(rr, httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString(`{"lib":"library","path":"new-folder"}`)))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected unauthorized mkdir, got %d", rr.Code)
		}

		rr = httptest.NewRecorder()
		body := bytes.NewBufferString(`{"lib":"library","path":"../escape"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/mkdir", body)
		req.Header.Set("Cookie", cookie)
		handlerMkdir.ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest && rr.Code != http.StatusForbidden {
			t.Fatalf("expected mkdir escape failure, got %d", rr.Code)
		}
	})

	t.Run("mkdir forbidden root and dry run", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString(`{"lib":"library","path":"new-folder"}`))
		req.Header.Set("Cookie", sessionCookie(t, "nobody", "token"))
		handlerMkdir.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden mkdir root, got %d", rr.Code)
		}

		orig := internal.DryRun
		internal.DryRun = true
		defer func() { internal.DryRun = orig }()
		rr = httptest.NewRecorder()
		req = httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString(`{"lib":"library","path":"new-folder"}`))
		req.Header.Set("Cookie", cookie)
		handlerMkdir.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("expected dry-run mkdir success, got %d", rr.Code)
		}
	})

	t.Run("mkdir create failure", func(t *testing.T) {
		orig := internal.DryRun
		internal.DryRun = false
		defer func() { internal.DryRun = orig }()

		blocker := filepath.Join(root, "library", "blocker")
		if err := os.WriteFile(blocker, []byte("file"), 0o644); err != nil {
			t.Fatalf("write blocker file: %v", err)
		}

		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString(`{"lib":"library","path":"blocker/child"}`))
		req.Header.Set("Cookie", cookie)
		handlerMkdir.ServeHTTP(rr, req)
		if rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected mkdir create failure, got %d", rr.Code)
		}
	})

	t.Run("mkdir empty path", func(t *testing.T) {
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodPost, "/api/mkdir", bytes.NewBufferString(`{"lib":"library","path":"   "}`))
		req.Header.Set("Cookie", cookie)
		handlerMkdir.ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected empty folder rejection, got %d", rr.Code)
		}
	})

	t.Run("mkdir missing library root", func(t *testing.T) {
		mediaRoot = filepath.Join(root, "file-root")
		if err := os.WriteFile(mediaRoot, []byte("not a dir"), 0o644); err != nil {
			t.Fatalf("write file root: %v", err)
		}
		rr := httptest.NewRecorder()
		body := bytes.NewBufferString(`{"lib":"library","path":"new-folder"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/mkdir", body)
		req.Header.Set("Cookie", cookie)
		handlerMkdir.ServeHTTP(rr, req)
		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected bad request for missing library root, got %d", rr.Code)
		}
	})

	t.Run("debug write unauthorized and dry run", func(t *testing.T) {
		handlerDebug := debugWriteTestHandler(um)

		rr := httptest.NewRecorder()
		handlerDebug.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/debug/write-test", nil))
		if rr.Code != http.StatusUnauthorized {
			t.Fatalf("expected unauthorized debug write, got %d", rr.Code)
		}

		orig := internal.DryRun
		internal.DryRun = true
		defer func() { internal.DryRun = orig }()
		rr = httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, "/debug/write-test", nil)
		req.Header.Set("Cookie", cookie)
		handlerDebug.ServeHTTP(rr, req)
		if rr.Code != http.StatusOK {
			t.Fatalf("expected dry-run debug write success, got %d", rr.Code)
		}
	})

	t.Run("debug write forbidden root", func(t *testing.T) {
		handlerDebug := debugWriteTestHandler(um)
		rr := httptest.NewRecorder()
		req := httptest.NewRequest(http.MethodGet, "/debug/write-test", nil)
		req.Header.Set("Cookie", sessionCookie(t, "nobody", "token"))
		handlerDebug.ServeHTTP(rr, req)
		if rr.Code != http.StatusForbidden {
			t.Fatalf("expected forbidden debug write root, got %d", rr.Code)
		}
	})
}
