package web

// handlers_files_test.go — Wave 1 (Plan 02) test implementation for FILE-01..03, FILE-06.
//
// TestFileUpload and TestFilesTab are activated (no t.Skip).
// TestFileDownload, TestFileDelete, TestFileOwnership (download/delete routes) remain
// as skipped stubs — those are wired in Plan 03.
//
// Pattern: mirrors handlers_tasks_test.go exactly — setupTestDB, loginUser,
// preInsertUser, getCSRFToken, and testCSRFKey are reused from the existing
// test files in the same package.

import (
	"bytes"
	"context"
	"errors"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"

	"backend/internal/auth"
	"backend/internal/db/sqlc"
	"backend/internal/files"

	"github.com/jackc/pgx/v5/pgtype"
)

// stubbedFileStorer is a no-op FileStorer for test injection.
// deleteErr may be set to simulate S3 delete failures (TestFileDelete_S3Failure).
type stubbedFileStorer struct {
	uploadedKey string
	deletedKey  string
	deleteErr   error
}

func (s *stubbedFileStorer) Upload(_ context.Context, key string, _ io.Reader) (string, int64, error) {
	s.uploadedKey = key
	return "application/octet-stream", 12, nil
}

func (s *stubbedFileStorer) Delete(_ context.Context, key string) error {
	s.deletedKey = key
	return s.deleteErr
}

func (s *stubbedFileStorer) PresignDownload(_ context.Context, _ string) (string, error) {
	return "https://example.com/signed?key=foo", nil
}

// Compile-time assertion: stubbedFileStorer satisfies the files.FileStorer interface.
var _ files.FileStorer = (*stubbedFileStorer)(nil)

// newFileTestRouter builds a router with FilesDeps wired for file handler tests.
func newFileTestRouter(q *sqlc.Queries, store *auth.Store, fileStore files.FileStorer) http.Handler {
	authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
	tabloDeps := TablosDeps{Queries: q}
	taskDeps := TasksDeps{Queries: q}
	fileDeps := FilesDeps{Queries: q, Files: fileStore, MaxUploadMB: 25}
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, DiscussionDeps{Queries: q}, PlanningDeps{Queries: q}, fileDeps, testCSRFKey, "dev", "localhost")
	if err != nil {
		panic("newFileTestRouter: " + err.Error())
	}
	return router
}

// ---- TestFileUpload (FILE-01, FILE-02) ----

// TestFileUpload verifies that POST /tablos/{id}/files uploads a file,
// creates a DB row, and stores bytes in S3 (FILE-01 server-proxied upload, FILE-02).
func TestFileUpload(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	user := preInsertUser(t, ctx, q, "fileupload@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Upload Test Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	// Build multipart body with a CSRF token.
	csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

	var body bytes.Buffer
	mw := multipart.NewWriter(&body)
	// Add CSRF field.
	_ = mw.WriteField("_csrf", csrfToken)
	// Add file field.
	fw, err := mw.CreateFormFile("file", "hello.txt")
	if err != nil {
		t.Fatalf("CreateFormFile: %v", err)
	}
	_, _ = fw.Write([]byte("hello world!"))
	mw.Close()

	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/files", &body)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	req.AddCookie(sessionCookie)
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	// Non-HTMX path: expect 303 redirect to /tablos/{id}/files.
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("POST /tablos/{id}/files status = %d; want 303\nbody: %s", rec.Code, rec.Body.String())
	}
	loc := rec.Header().Get("Location")
	if !strings.Contains(loc, "/files") {
		t.Errorf("redirect location = %q; want it to contain /files", loc)
	}

	// Verify a DB row was created.
	dbFiles, err := q.ListFilesByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListFilesByTablo: %v", err)
	}
	if len(dbFiles) != 1 {
		t.Fatalf("expected 1 file row, got %d", len(dbFiles))
	}
	if dbFiles[0].Filename != "hello.txt" {
		t.Errorf("filename = %q; want %q", dbFiles[0].Filename, "hello.txt")
	}

	// Verify S3 key starts with "files/{tablo_id}/" (D-04).
	if stub.uploadedKey == "" {
		t.Error("stub.uploadedKey is empty — Upload was not called")
	}
	expectedPrefix := "files/" + tablo.ID.String() + "/"
	if !strings.HasPrefix(stub.uploadedKey, expectedPrefix) {
		t.Errorf("S3 key = %q; want prefix %q", stub.uploadedKey, expectedPrefix)
	}
}

// TestFileUploadTooLarge verifies that POST with file body > MaxUploadMB returns 422
// with an error message containing "too large".
func TestFileUploadTooLarge(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}

	// Use 1 MB limit to keep test fast.
	authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
	tabloDeps := TablosDeps{Queries: q}
	taskDeps := TasksDeps{Queries: q}
	fileDeps := FilesDeps{Queries: q, Files: stub, MaxUploadMB: 1}
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, DiscussionDeps{Queries: q}, PlanningDeps{Queries: q}, fileDeps, testCSRFKey, "dev", "localhost")
	if err != nil {
		t.Fatalf("NewRouter: %v", err)
	}

	user := preInsertUser(t, ctx, q, "filelarge@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Large Upload Test Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

	// Build a multipart body that exceeds 1 MB.
	var body bytes.Buffer
	mw := multipart.NewWriter(&body)
	_ = mw.WriteField("_csrf", csrfToken)
	fw, err := mw.CreateFormFile("file", "big.bin")
	if err != nil {
		t.Fatalf("CreateFormFile: %v", err)
	}
	// Write 2 MB of data.
	bigChunk := make([]byte, 2*1024*1024)
	_, _ = fw.Write(bigChunk)
	mw.Close()

	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/files", &body)
	req.Header.Set("Content-Type", mw.FormDataContentType())
	req.AddCookie(sessionCookie)
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("oversized upload status = %d; want 422", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "too large") {
		t.Errorf("response body = %q; want it to contain 'too large'", rec.Body.String())
	}
}

// ---- TestFilesList (FILE-03) ----

// TestFilesList verifies that GET /tablos/{id}/files lists files with
// original filename and size, newest first (FILE-03).
func TestFilesList(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	user := preInsertUser(t, ctx, q, "fileslist@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "List Test Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	// Pre-insert a file row directly.
	_, err = q.InsertTabloFile(ctx, sqlc.InsertTabloFileParams{
		TabloID:     tablo.ID,
		S3Key:       "files/" + tablo.ID.String() + "/test-uuid",
		Filename:    "myfile.pdf",
		ContentType: "application/pdf",
		SizeBytes:   2048,
	})
	if err != nil {
		t.Fatalf("InsertTabloFile: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/files", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /tablos/{id}/files status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "myfile.pdf") {
		t.Errorf("file list missing filename 'myfile.pdf'; body snippet: %.200s", body)
	}
	// Human-readable size should appear (2048 bytes = "2.0 KB").
	if !strings.Contains(body, "KB") && !strings.Contains(body, "2048") {
		t.Errorf("file list missing size display; body snippet: %.200s", body)
	}
}

// ---- TestFilesTab (HTMX fragment) ----

// TestFilesTab verifies that GET /tablos/{id}/files with HX-Request: true
// returns 200 + HTML fragment (not full Layout), and without HX-Request returns full page.
func TestFilesTab(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	user := preInsertUser(t, ctx, q, "filestab@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Tab Test Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	// HX-Request path → should return fragment (no full <html> layout).
	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/files", nil)
	req.AddCookie(sessionCookie)
	req.Header.Set("HX-Request", "true")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("HTMX GET /tablos/{id}/files status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	// Fragment should NOT contain the full Layout <html> wrapper.
	if strings.Contains(body, "<html") {
		t.Errorf("HTMX response should be a fragment, not full page; body contains <html>")
	}
	// Should contain the upload form.
	if !strings.Contains(body, `name="file"`) {
		t.Errorf("HTMX response missing upload form file input; body snippet: %.200s", body)
	}

	// Non-HTMX path → should return full page with Layout.
	req2 := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/files", nil)
	req2.AddCookie(sessionCookie)
	rec2 := httptest.NewRecorder()
	router.ServeHTTP(rec2, req2)

	if rec2.Code != http.StatusOK {
		t.Fatalf("non-HTMX GET /tablos/{id}/files status = %d; want 200", rec2.Code)
	}
	body2 := rec2.Body.String()
	if !strings.Contains(body2, "<html") {
		t.Errorf("non-HTMX response should be full page with <html>; body snippet: %.200s", body2)
	}
}

// ---- TestFileDownload (FILE-04) ----

// TestFileDownload verifies that GET /tablos/{id}/files/{file_id}/download
// returns a 302 redirect to a signed time-limited URL (FILE-04).
func TestFileDownload(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	user := preInsertUser(t, ctx, q, "filedownload@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID: user.ID,
		Title:  "Download Test Tablo",
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	dbFile, err := q.InsertTabloFile(ctx, sqlc.InsertTabloFileParams{
		TabloID:     tablo.ID,
		S3Key:       "files/" + tablo.ID.String() + "/some-uuid",
		Filename:    "report.pdf",
		ContentType: "application/pdf",
		SizeBytes:   1024,
	})
	if err != nil {
		t.Fatalf("InsertTabloFile: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	url := "/tablos/" + tablo.ID.String() + "/files/" + dbFile.ID.String() + "/download"
	req := httptest.NewRequest(http.MethodGet, url, nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusFound {
		t.Fatalf("GET download status = %d; want 302\nbody: %s", rec.Code, rec.Body.String())
	}
	loc := rec.Header().Get("Location")
	if loc != "https://example.com/signed?key=foo" {
		t.Errorf("Location = %q; want %q", loc, "https://example.com/signed?key=foo")
	}
}

// TestFileDownload_NonOwner verifies that a non-owner gets 404 on download (FILE-06).
func TestFileDownload_NonOwner(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	owner := preInsertUser(t, ctx, q, "dlowner@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID: owner.ID,
		Title:  "Owner Tablo",
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	dbFile, err := q.InsertTabloFile(ctx, sqlc.InsertTabloFileParams{
		TabloID:     tablo.ID,
		S3Key:       "files/" + tablo.ID.String() + "/x",
		Filename:    "secret.pdf",
		ContentType: "application/pdf",
		SizeBytes:   512,
	})
	if err != nil {
		t.Fatalf("InsertTabloFile: %v", err)
	}

	nonOwner := preInsertUser(t, ctx, q, "dlnonowner@example.com", "correct-horse-12")
	sessionVal, _, err := store.Create(ctx, nonOwner.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	url := "/tablos/" + tablo.ID.String() + "/files/" + dbFile.ID.String() + "/download"
	req := httptest.NewRequest(http.MethodGet, url, nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusNotFound {
		t.Fatalf("non-owner download status = %d; want 404", rec.Code)
	}
}

// ---- TestFileDelete (FILE-05) ----

// TestFileDelete verifies that POST /tablos/{id}/files/{file_id}/delete
// removes the DB row, calls S3 delete, and returns the FileRowGone fragment for HTMX (FILE-05).
func TestFileDelete(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	user := preInsertUser(t, ctx, q, "filedelete@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID: user.ID,
		Title:  "Delete Test Tablo",
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	dbFile, err := q.InsertTabloFile(ctx, sqlc.InsertTabloFileParams{
		TabloID:     tablo.ID,
		S3Key:       "files/" + tablo.ID.String() + "/del-uuid",
		Filename:    "todelete.txt",
		ContentType: "text/plain",
		SizeBytes:   100,
	})
	if err != nil {
		t.Fatalf("InsertTabloFile: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	deleteURL := "/tablos/" + tablo.ID.String() + "/files/" + dbFile.ID.String() + "/delete"
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/files", []*http.Cookie{sessionCookie})

	var body strings.Builder
	body.WriteString(url.Values{"_csrf": {csrfToken}}.Encode())

	req := httptest.NewRequest(http.MethodPost, deleteURL, strings.NewReader(body.String()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("HTMX delete status = %d; want 200\nbody: %s", rec.Code, rec.Body.String())
	}

	// Response should contain the gone zone for the file.
	respBody := rec.Body.String()
	if !strings.Contains(respBody, "file-"+dbFile.ID.String()) {
		t.Errorf("response missing file-row-zone id; body: %.300s", respBody)
	}

	// S3 delete should have been called.
	if stub.deletedKey != dbFile.S3Key {
		t.Errorf("stub.deletedKey = %q; want %q", stub.deletedKey, dbFile.S3Key)
	}

	// DB row should no longer exist.
	remaining, err := q.ListFilesByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListFilesByTablo: %v", err)
	}
	if len(remaining) != 0 {
		t.Errorf("expected 0 files after delete, got %d", len(remaining))
	}
}

// TestFileDelete_S3Failure verifies that when S3 delete fails, the DB row is still
// deleted and the response is 200 (not 500) for the HTMX path (FILE-05 deviation pattern).
func TestFileDelete_S3Failure(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{deleteErr: errors.New("s3 unavailable")}
	router := newFileTestRouter(q, store, stub)

	user := preInsertUser(t, ctx, q, "dels3fail@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID: user.ID,
		Title:  "S3 Fail Delete Tablo",
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	dbFile, err := q.InsertTabloFile(ctx, sqlc.InsertTabloFileParams{
		TabloID:     tablo.ID,
		S3Key:       "files/" + tablo.ID.String() + "/s3fail-uuid",
		Filename:    "s3fail.txt",
		ContentType: "text/plain",
		SizeBytes:   50,
	})
	if err != nil {
		t.Fatalf("InsertTabloFile: %v", err)
	}

	sessionVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	deleteURL := "/tablos/" + tablo.ID.String() + "/files/" + dbFile.ID.String() + "/delete"
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/files", []*http.Cookie{sessionCookie})

	var body strings.Builder
	body.WriteString(url.Values{"_csrf": {csrfToken}}.Encode())

	req := httptest.NewRequest(http.MethodPost, deleteURL, strings.NewReader(body.String()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	// Even when S3 fails, HTMX path returns 200 (not 500).
	if rec.Code != http.StatusOK {
		t.Fatalf("S3 failure delete status = %d; want 200\nbody: %s", rec.Code, rec.Body.String())
	}

	// DB row should still be deleted.
	remaining, err := q.ListFilesByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListFilesByTablo: %v", err)
	}
	if len(remaining) != 0 {
		t.Errorf("expected 0 files after delete (S3 fail), got %d", len(remaining))
	}
}

// ---- TestFileOwnership (FILE-06) ----

// TestFileOwnership verifies that a non-owner gets 404 on download, delete-confirm,
// and delete routes (FILE-06).
func TestFileOwnership(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	stub := &stubbedFileStorer{}
	router := newFileTestRouter(q, store, stub)

	owner := preInsertUser(t, ctx, q, "ownship@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID: owner.ID,
		Title:  "Ownership Tablo",
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	dbFile, err := q.InsertTabloFile(ctx, sqlc.InsertTabloFileParams{
		TabloID:     tablo.ID,
		S3Key:       "files/" + tablo.ID.String() + "/own-uuid",
		Filename:    "owned.txt",
		ContentType: "text/plain",
		SizeBytes:   10,
	})
	if err != nil {
		t.Fatalf("InsertTabloFile: %v", err)
	}

	nonOwner := preInsertUser(t, ctx, q, "nonownship@example.com", "correct-horse-12")
	sessionVal, _, err := store.Create(ctx, nonOwner.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: sessionVal}

	base := "/tablos/" + tablo.ID.String() + "/files/" + dbFile.ID.String()

	// Non-owner: GET download → 404
	reqDL := httptest.NewRequest(http.MethodGet, base+"/download", nil)
	reqDL.AddCookie(sessionCookie)
	recDL := httptest.NewRecorder()
	router.ServeHTTP(recDL, reqDL)
	if recDL.Code != http.StatusNotFound {
		t.Errorf("non-owner download status = %d; want 404", recDL.Code)
	}

	// Non-owner: GET delete-confirm → 404
	reqDC := httptest.NewRequest(http.MethodGet, base+"/delete-confirm", nil)
	reqDC.AddCookie(sessionCookie)
	recDC := httptest.NewRecorder()
	router.ServeHTTP(recDC, reqDC)
	if recDC.Code != http.StatusNotFound {
		t.Errorf("non-owner delete-confirm status = %d; want 404", recDC.Code)
	}

	// Non-owner: POST delete → 404
	// We need a CSRF token from non-owner's session to get past CSRF middleware.
	csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})
	var delBody strings.Builder
	delBody.WriteString(url.Values{"_csrf": {csrfToken}}.Encode())
	reqDel := httptest.NewRequest(http.MethodPost, base+"/delete", strings.NewReader(delBody.String()))
	reqDel.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	reqDel.AddCookie(sessionCookie)
	for _, c := range csrfCookies {
		reqDel.AddCookie(c)
	}
	recDel := httptest.NewRecorder()
	router.ServeHTTP(recDel, reqDel)
	if recDel.Code != http.StatusNotFound {
		t.Errorf("non-owner delete status = %d; want 404", recDel.Code)
	}
}
