//go:build integration

package catalog_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"github.com/get-drexa/drexa/internal/account"
	"github.com/get-drexa/drexa/internal/auth"
	"github.com/get-drexa/drexa/internal/blob"
	"github.com/get-drexa/drexa/internal/catalog"
	"github.com/get-drexa/drexa/internal/database"
	"github.com/get-drexa/drexa/internal/drive"
	"github.com/get-drexa/drexa/internal/organization"
	"github.com/get-drexa/drexa/internal/registration"
	"github.com/get-drexa/drexa/internal/sharing"
	"github.com/get-drexa/drexa/internal/user"
	"github.com/get-drexa/drexa/internal/virtualfs"
	"github.com/gofiber/fiber/v2"
	"github.com/google/uuid"
	"github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/uptrace/bun"
)

type testEnv struct {
	db          *bun.DB
	vfs         *virtualfs.VirtualFS
	app         *fiber.App
	accessToken string
	driveID     string
	rootDirID   uuid.UUID
	scope       *virtualfs.Scope
	orgSlug     string
}

func setupTestEnv(t *testing.T, ctx context.Context) *testEnv {
	t.Helper()

	pg, err := runPostgres(ctx)
	if err != nil {
		t.Skipf("postgres testcontainer unavailable: %v", err)
	}
	t.Cleanup(func() { _ = pg.Terminate(ctx) })

	postgresURL, err := pg.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		t.Fatalf("postgres connection string: %v", err)
	}

	db := database.NewFromPostgres(postgresURL)
	t.Cleanup(func() { _ = db.Close() })

	if err := database.RunMigrations(ctx, db); err != nil {
		t.Fatalf("RunMigrations: %v", err)
	}

	blobRoot, err := os.MkdirTemp("", "drexa-blobs-*")
	if err != nil {
		t.Fatalf("temp blob dir: %v", err)
	}
	t.Cleanup(func() { _ = os.RemoveAll(blobRoot) })

	blobStore := blob.NewFSStore(blob.FSStoreConfig{Root: blobRoot})
	if err := blobStore.Initialize(ctx); err != nil {
		t.Fatalf("blob store init: %v", err)
	}

	vfs, err := virtualfs.New(blobStore, virtualfs.NewFlatKeyResolver())
	if err != nil {
		t.Fatalf("virtualfs.New: %v", err)
	}

	userSvc := user.NewService()
	orgSvc := organization.NewService()
	accSvc := account.NewService()
	driveSvc := drive.NewService()
	sharingSvc, err := sharing.NewService(vfs)
	if err != nil {
		t.Fatalf("sharing.NewService: %v", err)
	}
	regSvc := registration.NewService(userSvc, orgSvc, accSvc, driveSvc, vfs)

	regResult, err := regSvc.Register(ctx, db, registration.RegisterOptions{
		Email:       "test@example.com",
		Password:    "password123",
		DisplayName: "Test User",
	})
	if err != nil {
		t.Fatalf("Register: %v", err)
	}

	authSvc := auth.NewService(userSvc, auth.TokenConfig{
		Issuer:    "drexa-test",
		Audience:  "drexa-test",
		SecretKey: []byte("drexa-test-secret"),
	})

	grant, err := authSvc.GrantForUser(ctx, db, regResult.User)
	if err != nil {
		t.Fatalf("GrantForUser: %v", err)
	}

	authMiddleware := auth.NewAuthMiddleware(authSvc, db, auth.CookieConfig{})

	root, err := vfs.FindRootDirectory(ctx, db, regResult.Drive.ID)
	if err != nil {
		t.Fatalf("FindRootDirectory: %v", err)
	}

	scope := &virtualfs.Scope{
		DriveID:    regResult.Drive.ID,
		RootNodeID: root.ID,
		AllowedOps: virtualfs.AllAllowedOps,
		ActorKind:  virtualfs.ScopeActorAccount,
		ActorID:    regResult.Account.ID,
	}

	app := fiber.New()
	api := app.Group("/api")

	// Set up org-scoped routes like the real server does
	orgMiddleware := organization.NewMiddleware(orgSvc, accSvc, db)
	orgAPI := api.Group("/:orgSlug", authMiddleware, orgMiddleware)

	driveHTTP := drive.NewHTTPHandler(driveSvc, accSvc, vfs, db, authMiddleware)
	scopedRouter := driveHTTP.RegisterRoutes(orgAPI)

	catalogHTTP := catalog.NewHTTPHandler(sharingSvc, vfs, db)
	catalogHTTP.RegisterRoutes(scopedRouter)

	return &testEnv{
		db:          db,
		vfs:         vfs,
		app:         app,
		accessToken: grant.AccessToken,
		driveID:     regResult.Drive.PublicID,
		rootDirID:   root.ID,
		scope:       scope,
		orgSlug:     organization.ReservedSlug,
	}
}

func TestHTTP_CatalogEndpoints(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	env := setupTestEnv(t, ctx)

	// Create test data using VFS directly:
	// - 2 files in root: file1.txt, file2.txt
	// - 1 directory in root: subdir
	// - 1 file in subdir: nested.txt

	file1, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
		ParentID: env.rootDirID,
		Name:     "file1.txt",
	}, env.scope)
	if err != nil {
		t.Fatalf("CreateFile file1: %v", err)
	}
	err = env.vfs.WriteFile(ctx, env.db, file1, virtualfs.FileContentFromReader(bytes.NewReader([]byte("content1"))), env.scope)
	if err != nil {
		t.Fatalf("WriteFile file1: %v", err)
	}

	file2, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
		ParentID: env.rootDirID,
		Name:     "file2.txt",
	}, env.scope)
	if err != nil {
		t.Fatalf("CreateFile file2: %v", err)
	}
	err = env.vfs.WriteFile(ctx, env.db, file2, virtualfs.FileContentFromReader(bytes.NewReader([]byte("content2"))), env.scope)
	if err != nil {
		t.Fatalf("WriteFile file2: %v", err)
	}

	subdir, err := env.vfs.CreateDirectory(ctx, env.db, env.rootDirID, "subdir", env.scope)
	if err != nil {
		t.Fatalf("CreateDirectory subdir: %v", err)
	}

	nestedFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
		ParentID: subdir.ID,
		Name:     "nested.txt",
	}, env.scope)
	if err != nil {
		t.Fatalf("CreateFile nested: %v", err)
	}
	err = env.vfs.WriteFile(ctx, env.db, nestedFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("nested content"))), env.scope)
	if err != nil {
		t.Fatalf("WriteFile nested: %v", err)
	}

	// Helper to build URLs with org slug
	driveURL := func(path string) string {
		return fmt.Sprintf("/api/%s/drives/%s%s", env.orgSlug, env.driveID, path)
	}

	// Directory tests
	t.Run("list root directory", func(t *testing.T) {
		var resp struct {
			Items []struct {
				Kind string `json:"kind"`
				ID   string `json:"id"`
				Name string `json:"name"`
			} `json:"items"`
		}
		doJSONAuth(t, env.app, http.MethodGet, driveURL("/directories/root/content"), env.accessToken, nil, http.StatusOK, &resp)

		if len(resp.Items) != 3 {
			t.Fatalf("expected 3 items in root, got %d", len(resp.Items))
		}

		names := map[string]string{}
		for _, item := range resp.Items {
			names[item.Name] = item.Kind
		}
		if names["file1.txt"] != "file" {
			t.Errorf("expected file1.txt to be a file")
		}
		if names["file2.txt"] != "file" {
			t.Errorf("expected file2.txt to be a file")
		}
		if names["subdir"] != "directory" {
			t.Errorf("expected subdir to be a directory")
		}
	})

	t.Run("fetch directory by ID", func(t *testing.T) {
		var resp struct {
			Kind string `json:"kind"`
			ID   string `json:"id"`
			Name string `json:"name"`
		}
		doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/directories/%s", subdir.PublicID)), env.accessToken, nil, http.StatusOK, &resp)

		if resp.Kind != "directory" {
			t.Errorf("expected kind=directory, got %q", resp.Kind)
		}
		if resp.Name != "subdir" {
			t.Errorf("expected name=subdir, got %q", resp.Name)
		}
		if resp.ID != subdir.PublicID {
			t.Errorf("expected id=%s, got %s", subdir.PublicID, resp.ID)
		}
	})

	t.Run("list subdir contents", func(t *testing.T) {
		var resp struct {
			Items []struct {
				Kind string `json:"kind"`
				ID   string `json:"id"`
				Name string `json:"name"`
			} `json:"items"`
		}
		doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/directories/%s/content", subdir.PublicID)), env.accessToken, nil, http.StatusOK, &resp)

		if len(resp.Items) != 1 {
			t.Fatalf("expected 1 item in subdir, got %d", len(resp.Items))
		}
		if resp.Items[0].Name != "nested.txt" {
			t.Errorf("expected nested.txt, got %q", resp.Items[0].Name)
		}
		if resp.Items[0].Kind != "file" {
			t.Errorf("expected kind=file, got %q", resp.Items[0].Kind)
		}
	})

	t.Run("create directory", func(t *testing.T) {
		root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
		if err != nil {
			t.Fatalf("FindNode root: %v", err)
		}

		var resp struct {
			Kind string `json:"kind"`
			ID   string `json:"id"`
			Name string `json:"name"`
		}
		body := map[string]string{"parentID": root.PublicID, "name": "newdir"}
		doJSONAuth(t, env.app, http.MethodPost, driveURL("/directories"), env.accessToken, body, http.StatusOK, &resp)

		if resp.Kind != "directory" {
			t.Errorf("expected kind=directory, got %q", resp.Kind)
		}
		if resp.Name != "newdir" {
			t.Errorf("expected name=newdir, got %q", resp.Name)
		}
		if resp.ID == "" {
			t.Errorf("expected non-empty ID")
		}
	})

	t.Run("rename directory", func(t *testing.T) {
		root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
		if err != nil {
			t.Fatalf("FindNode root: %v", err)
		}
		renameDir, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "to-rename", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory to-rename: %v", err)
		}

		var resp struct {
			Kind string `json:"kind"`
			ID   string `json:"id"`
			Name string `json:"name"`
		}
		body := map[string]string{"name": "renamed-dir"}
		doJSONAuth(t, env.app, http.MethodPatch, driveURL(fmt.Sprintf("/directories/%s", renameDir.PublicID)), env.accessToken, body, http.StatusOK, &resp)

		if resp.Kind != "directory" {
			t.Errorf("expected kind=directory, got %q", resp.Kind)
		}
		if resp.Name != "renamed-dir" {
			t.Errorf("expected name=renamed-dir, got %q", resp.Name)
		}
	})

	// File tests
	t.Run("fetch file by ID", func(t *testing.T) {
		var resp struct {
			Kind     string `json:"kind"`
			ID       string `json:"id"`
			Name     string `json:"name"`
			Size     int64  `json:"size"`
			MimeType string `json:"mimeType"`
		}
		doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/files/%s", file1.PublicID)), env.accessToken, nil, http.StatusOK, &resp)

		if resp.Kind != "file" {
			t.Errorf("expected kind=file, got %q", resp.Kind)
		}
		if resp.Name != "file1.txt" {
			t.Errorf("expected name=file1.txt, got %q", resp.Name)
		}
		if resp.Size != 8 {
			t.Errorf("expected size=8, got %d", resp.Size)
		}
	})

	t.Run("download file content", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s/content", file1.PublicID)), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		defer res.Body.Close()

		if res.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(res.Body)
			t.Fatalf("expected 200, got %d: %s", res.StatusCode, body)
		}

		content, _ := io.ReadAll(res.Body)
		if string(content) != "content1" {
			t.Errorf("expected content1, got %q", string(content))
		}
	})

	t.Run("rename file", func(t *testing.T) {
		// Create a fresh file to rename (must write content to set status to ready)
		renameFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "to-rename.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile to-rename: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, renameFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("rename me"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile to-rename: %v", err)
		}

		var resp struct {
			Kind string `json:"kind"`
			ID   string `json:"id"`
			Name string `json:"name"`
		}
		body := map[string]string{"name": "renamed-file.txt"}
		doJSONAuth(t, env.app, http.MethodPatch, driveURL(fmt.Sprintf("/files/%s", renameFile.PublicID)), env.accessToken, body, http.StatusOK, &resp)

		if resp.Name != "renamed-file.txt" {
			t.Errorf("expected name=renamed-file.txt, got %q", resp.Name)
		}
	})

	t.Run("fetch nested file", func(t *testing.T) {
		var resp struct {
			Kind string `json:"kind"`
			ID   string `json:"id"`
			Name string `json:"name"`
			Size int64  `json:"size"`
		}
		doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/files/%s", nestedFile.PublicID)), env.accessToken, nil, http.StatusOK, &resp)

		if resp.Name != "nested.txt" {
			t.Errorf("expected name=nested.txt, got %q", resp.Name)
		}
		if resp.Size != 14 {
			t.Errorf("expected size=14, got %d", resp.Size)
		}
	})

	t.Run("download nested file content", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s/content", nestedFile.PublicID)), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		defer res.Body.Close()

		if res.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(res.Body)
			t.Fatalf("expected 200, got %d: %s", res.StatusCode, body)
		}

		content, _ := io.ReadAll(res.Body)
		if string(content) != "nested content" {
			t.Errorf("expected 'nested content', got %q", string(content))
		}
	})

	// Delete tests
	t.Run("trash file", func(t *testing.T) {
		// Create a fresh file to trash (must write content to set status to ready)
		trashFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "to-trash.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile to-trash: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, trashFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("trash me"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile to-trash: %v", err)
		}

		var resp struct {
			ID   string `json:"id"`
			Name string `json:"name"`
			Kind string `json:"kind"`
		}
		doJSONAuth(t, env.app, http.MethodDelete, driveURL(fmt.Sprintf("/files/%s?trash=true", trashFile.PublicID)), env.accessToken, nil, http.StatusOK, &resp)

		if resp.ID != trashFile.PublicID {
			t.Errorf("expected id=%s, got %s", trashFile.PublicID, resp.ID)
		}
		if resp.Kind != "file" {
			t.Errorf("expected kind=file, got %q", resp.Kind)
		}
	})

	t.Run("delete file permanently", func(t *testing.T) {
		// Create a temp file to delete (must write content to set status to ready)
		tempFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "temp.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile temp: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, tempFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("delete me"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile temp: %v", err)
		}

		req := httptest.NewRequest(http.MethodDelete, driveURL(fmt.Sprintf("/files/%s", tempFile.PublicID)), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNoContent {
			t.Errorf("expected 204, got %d", res.StatusCode)
		}

		// Verify file is gone
		req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s", tempFile.PublicID)), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err = env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNotFound {
			t.Errorf("expected 404 after permanent delete, got %d", res.StatusCode)
		}
	})

	t.Run("404 for non-existent file", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, driveURL("/files/nonexistent123"), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNotFound {
			t.Errorf("expected 404, got %d", res.StatusCode)
		}
	})

	t.Run("404 for non-existent directory", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, driveURL("/directories/nonexistent123"), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNotFound {
			t.Errorf("expected 404, got %d", res.StatusCode)
		}
	})

	// Bulk delete tests
	t.Run("bulk trash files", func(t *testing.T) {
		// Create temp files for bulk delete (must write content to set status to ready)
		bulkFile1, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "bulk1.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile bulk1: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, bulkFile1, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk1"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile bulk1: %v", err)
		}

		bulkFile2, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "bulk2.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile bulk2: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, bulkFile2, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk2"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile bulk2: %v", err)
		}

		var resp []struct {
			ID   string `json:"id"`
			Name string `json:"name"`
			Kind string `json:"kind"`
		}
		url := driveURL(fmt.Sprintf("/files?id=%s,%s&trash=true", bulkFile1.PublicID, bulkFile2.PublicID))
		doJSONAuth(t, env.app, http.MethodDelete, url, env.accessToken, nil, http.StatusOK, &resp)

		if len(resp) != 2 {
			t.Fatalf("expected 2 trashed files, got %d", len(resp))
		}
		for _, f := range resp {
			if f.Kind != "file" {
				t.Errorf("expected kind=file for %s, got %q", f.Name, f.Kind)
			}
		}
	})

	t.Run("bulk delete files permanently", func(t *testing.T) {
		// Create temp files for bulk delete (must write content to set status to ready)
		bulkFile3, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "bulk3.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile bulk3: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, bulkFile3, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk3"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile bulk3: %v", err)
		}

		bulkFile4, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
			ParentID: env.rootDirID,
			Name:     "bulk4.txt",
		}, env.scope)
		if err != nil {
			t.Fatalf("CreateFile bulk4: %v", err)
		}
		err = env.vfs.WriteFile(ctx, env.db, bulkFile4, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk4"))), env.scope)
		if err != nil {
			t.Fatalf("WriteFile bulk4: %v", err)
		}

		url := driveURL(fmt.Sprintf("/files?id=%s,%s", bulkFile3.PublicID, bulkFile4.PublicID))
		req := httptest.NewRequest(http.MethodDelete, url, nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNoContent {
			t.Errorf("expected 204, got %d", res.StatusCode)
		}

		// Verify files are gone
		for _, fileID := range []string{bulkFile3.PublicID, bulkFile4.PublicID} {
			req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s", fileID)), nil)
			req.Header.Set("Authorization", "Bearer "+env.accessToken)

			res, err = env.app.Test(req, 10_000)
			if err != nil {
				t.Fatalf("request failed: %v", err)
			}
			res.Body.Close()

			if res.StatusCode != http.StatusNotFound {
				t.Errorf("expected 404 for %s after permanent delete, got %d", fileID, res.StatusCode)
			}
		}
	})

	t.Run("bulk trash directories", func(t *testing.T) {
		root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
		if err != nil {
			t.Fatalf("FindNode root: %v", err)
		}

		// Create temp directories for bulk delete
		bulkDir1, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir1", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory bulkdir1: %v", err)
		}
		bulkDir2, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir2", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory bulkdir2: %v", err)
		}

		var resp []struct {
			ID        string  `json:"id"`
			Name      string  `json:"name"`
			DeletedAt *string `json:"deletedAt"`
		}
		url := driveURL(fmt.Sprintf("/directories?id=%s,%s&trash=true", bulkDir1.PublicID, bulkDir2.PublicID))
		doJSONAuth(t, env.app, http.MethodDelete, url, env.accessToken, nil, http.StatusOK, &resp)

		if len(resp) != 2 {
			t.Fatalf("expected 2 trashed directories, got %d", len(resp))
		}
		for _, d := range resp {
			if d.DeletedAt == nil {
				t.Errorf("expected deletedAt to be set for %s", d.Name)
			}
		}
	})

	t.Run("bulk delete directories permanently", func(t *testing.T) {
		root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
		if err != nil {
			t.Fatalf("FindNode root: %v", err)
		}

		// Create temp directories for bulk delete
		bulkDir3, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir3", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory bulkdir3: %v", err)
		}
		bulkDir4, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir4", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory bulkdir4: %v", err)
		}

		url := driveURL(fmt.Sprintf("/directories?id=%s,%s", bulkDir3.PublicID, bulkDir4.PublicID))
		req := httptest.NewRequest(http.MethodDelete, url, nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNoContent {
			t.Errorf("expected 204, got %d", res.StatusCode)
		}

		// Verify directories are gone
		for _, dirID := range []string{bulkDir3.PublicID, bulkDir4.PublicID} {
			req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/directories/%s", dirID)), nil)
			req.Header.Set("Authorization", "Bearer "+env.accessToken)

			res, err = env.app.Test(req, 10_000)
			if err != nil {
				t.Fatalf("request failed: %v", err)
			}
			res.Body.Close()

			if res.StatusCode != http.StatusNotFound {
				t.Errorf("expected 404 for %s after permanent delete, got %d", dirID, res.StatusCode)
			}
		}
	})

	t.Run("delete directory with trash", func(t *testing.T) {
		root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
		if err != nil {
			t.Fatalf("FindNode root: %v", err)
		}

		trashDir, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "trashme", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory trashme: %v", err)
		}

		var resp struct {
			ID        string  `json:"id"`
			Name      string  `json:"name"`
			DeletedAt *string `json:"deletedAt"`
		}
		doJSONAuth(t, env.app, http.MethodDelete, driveURL(fmt.Sprintf("/directories/%s?trash=true", trashDir.PublicID)), env.accessToken, nil, http.StatusOK, &resp)

		if resp.DeletedAt == nil {
			t.Errorf("expected deletedAt to be set")
		}
	})

	t.Run("delete directory permanently", func(t *testing.T) {
		root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
		if err != nil {
			t.Fatalf("FindNode root: %v", err)
		}

		permDir, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "deleteme", env.scope)
		if err != nil {
			t.Fatalf("CreateDirectory deleteme: %v", err)
		}

		req := httptest.NewRequest(http.MethodDelete, driveURL(fmt.Sprintf("/directories/%s", permDir.PublicID)), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err := env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNoContent {
			t.Errorf("expected 204, got %d", res.StatusCode)
		}

		// Verify directory is gone
		req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/directories/%s", permDir.PublicID)), nil)
		req.Header.Set("Authorization", "Bearer "+env.accessToken)

		res, err = env.app.Test(req, 10_000)
		if err != nil {
			t.Fatalf("request failed: %v", err)
		}
		res.Body.Close()

		if res.StatusCode != http.StatusNotFound {
			t.Errorf("expected 404 after permanent delete, got %d", res.StatusCode)
		}
	})
}

func doJSONAuth(
	t *testing.T,
	app *fiber.App,
	method string,
	path string,
	accessToken string,
	body any,
	wantStatus int,
	out any,
) {
	t.Helper()

	var reqBody *bytes.Reader
	if body == nil {
		reqBody = bytes.NewReader(nil)
	} else {
		b, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("json marshal: %v", err)
		}
		reqBody = bytes.NewReader(b)
	}

	req := httptest.NewRequest(method, path, reqBody)
	req.Header.Set("Content-Type", "application/json")
	if accessToken != "" {
		req.Header.Set("Authorization", "Bearer "+accessToken)
	}

	res, err := app.Test(req, 10_000)
	if err != nil {
		t.Fatalf("%s %s: %v", method, path, err)
	}
	defer res.Body.Close()

	if res.StatusCode != wantStatus {
		gotBody, _ := io.ReadAll(res.Body)
		t.Fatalf("%s %s: status %d want %d body=%s", method, path, res.StatusCode, wantStatus, string(gotBody))
	}

	if out == nil {
		return
	}
	if err := json.NewDecoder(res.Body).Decode(out); err != nil {
		t.Fatalf("%s %s: decode response: %v", method, path, err)
	}
}

func runPostgres(ctx context.Context) (_ *postgres.PostgresContainer, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("testcontainers panic: %v", r)
		}
	}()

	return postgres.Run(
		ctx,
		"postgres:16-alpine",
		postgres.WithDatabase("drexa"),
		postgres.WithUsername("drexa"),
		postgres.WithPassword("drexa"),
		postgres.BasicWaitStrategies(),
	)
}
