package server

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"

	"github.com/photoprism/photoprism/internal/config"
	"github.com/photoprism/photoprism/internal/entity"
	"github.com/photoprism/photoprism/pkg/http/header"
)

func setupWebDAVRouter(conf *config.Config) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	grp := r.Group(conf.BaseUri(WebDAVOriginals), WebDAVAuth(conf))
	WebDAV(conf.OriginalsPath(), grp, conf)
	return r
}

func authBearer(req *http.Request) {
	sess := entity.SessionFixtures.Get("alice_token_webdav")
	header.SetAuthorization(req, sess.AuthToken())
}

func authBasic(req *http.Request) {
	sess := entity.SessionFixtures.Get("alice_token_webdav")
	basic := []byte(fmt.Sprintf("alice:%s", sess.AuthToken()))
	req.Header.Set(header.Auth, fmt.Sprintf("%s %s", header.AuthBasic, base64.StdEncoding.EncodeToString(basic)))
}

func TestWebDAVWrite_MKCOL_PUT(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)

	// MKCOL
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/wdvdir", nil)
	authBearer(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1) // Created
	// PUT file
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/wdvdir/hello.txt", bytes.NewBufferString("hello"))
	authBearer(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)
	// file exists
	path := filepath.Join(conf.OriginalsPath(), "wdvdir", "hello.txt")
	// #nosec G304 -- test reads file created under controlled temp directory.
	b, err := os.ReadFile(path)
	assert.NoError(t, err)
	assert.Equal(t, "hello", string(b))
}

func TestWebDAVWrite_MOVE_COPY(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)

	// Ensure source and destination directories via MKCOL
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/src", nil)
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/dst", nil)
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)
	// Create source file via PUT
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/src/a.txt", bytes.NewBufferString("A"))
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)

	// MOVE /originals/src/a.txt -> /originals/dst/b.txt
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/src/a.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/b.txt")
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)
	// Verify moved
	assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "src", "a.txt"))
	assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "dst", "b.txt"))

	// COPY /originals/dst/b.txt -> /originals/dst/c.txt
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/dst/b.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/c.txt")
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)
	// Verify copy
	assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "dst", "b.txt"))
	assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "dst", "c.txt"))
}

func TestWebDAVWrite_OverwriteSemantics(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)

	// Prepare src and dst
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "src"), 0o700)
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "dst"), 0o700)
	_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "src", "f.txt"), []byte("NEW"), 0o600)
	_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "dst", "f.txt"), []byte("OLD"), 0o600)

	// COPY with Overwrite: F -> should not overwrite existing
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/src/f.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/f.txt")
	req.Header.Set("Overwrite", "F")
	authBasic(req)
	r.ServeHTTP(w, req)
	// Expect not successful (commonly 412 Precondition Failed)
	if w.Code == 201 || w.Code == 204 {
		t.Fatalf("expected failure when Overwrite=F, got %d", w.Code)
	}
	// Content remains OLD
	b, _ := os.ReadFile(filepath.Join(conf.OriginalsPath(), "dst", "f.txt"))
	assert.Equal(t, "OLD", string(b))

	// COPY with Overwrite: T -> must overwrite
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/src/f.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/f.txt")
	req.Header.Set("Overwrite", "T")
	authBasic(req)
	r.ServeHTTP(w, req)
	// Success (201/204 acceptable)
	if w.Code != http.StatusCreated && w.Code != http.StatusNoContent {
		t.Fatalf("expected success for Overwrite=T, got %d", w.Code)
	}
	b, _ = os.ReadFile(filepath.Join(conf.OriginalsPath(), "dst", "f.txt"))
	assert.Equal(t, "NEW", string(b))

	// MOVE with Overwrite: F to existing file -> expect failure
	_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "src", "g.txt"), []byte("GNEW"), 0o600)
	_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "dst", "g.txt"), []byte("GOLD"), 0o600)
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/src/g.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/g.txt")
	req.Header.Set("Overwrite", "F")
	authBasic(req)
	r.ServeHTTP(w, req)
	if w.Code == 201 || w.Code == 204 {
		t.Fatalf("expected failure when Overwrite=F for MOVE, got %d", w.Code)
	}
	// MOVE with Overwrite: T -> overwrites and removes source
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/src/g.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/g.txt")
	req.Header.Set("Overwrite", "T")
	authBasic(req)
	r.ServeHTTP(w, req)
	if w.Code != http.StatusCreated && w.Code != http.StatusNoContent {
		t.Fatalf("expected success for MOVE Overwrite=T, got %d", w.Code)
	}
	assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "src", "g.txt"))
	gb, _ := os.ReadFile(filepath.Join(conf.OriginalsPath(), "dst", "g.txt"))
	assert.Equal(t, "GNEW", string(gb))
}

func TestWebDAVWrite_MoveMissingDestination(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)
	// Ensure src exists
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "mv"), 0o700)
	_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "mv", "file.txt"), []byte("X"), 0o600)

	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/mv/file.txt", nil)
	// no Destination header
	authBasic(req)
	r.ServeHTTP(w, req)
	// Expect failure (not 201/204)
	if w.Code == http.StatusCreated || w.Code == http.StatusNoContent {
		t.Fatalf("expected failure when Destination header missing, got %d", w.Code)
	}
	// Source remains
	assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "mv", "file.txt"))
}

func TestWebDAVWrite_CopyInvalidDestinationPrefix(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)
	// Ensure src exists
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "cp"), 0o700)
	_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "cp", "a.txt"), []byte("A"), 0o600)

	// COPY to a destination outside the handler prefix
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/cp/a.txt", nil)
	req.Header.Set("Destination", "/notwebdav/d.txt")
	authBasic(req)
	r.ServeHTTP(w, req)
	// Expect failure
	if w.Code == http.StatusCreated || w.Code == http.StatusNoContent {
		t.Fatalf("expected failure for invalid Destination prefix, got %d", w.Code)
	}
	// Destination not created
	assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "notwebdav", "d.txt"))
}

func TestWebDAVWrite_MoveNonExistentSource(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)
	// Ensure destination dir exists
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "dst2"), 0o700)

	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/nosuch/file.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst2/file.txt")
	authBasic(req)
	r.ServeHTTP(w, req)
	// Expect failure (e.g., 404)
	if w.Code == http.StatusCreated || w.Code == http.StatusNoContent {
		t.Fatalf("expected failure moving non-existent source, got %d", w.Code)
	}
	assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "dst2", "file.txt"))
}

func TestWebDAVWrite_CopyTraversalDestination(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)

	// Create source file via PUT
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "travsrc"), 0o700)
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/travsrc/a.txt", bytes.NewBufferString("A"))
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)

	// Attempt COPY with traversal in Destination
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/travsrc/a.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/../evil.txt")
	authBasic(req)
	r.ServeHTTP(w, req)
	// Expect success with sanitized destination inside base
	if w.Code != http.StatusCreated && w.Code != http.StatusNoContent {
		t.Fatalf("expected success (sanitized), got %d", w.Code)
	}
	// Not created above originals; created as /originals/evil.txt
	parent := filepath.Dir(conf.OriginalsPath())
	assert.NoFileExists(t, filepath.Join(parent, "evil.txt"))
	assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "evil.txt"))
}

func TestWebDAVWrite_MoveTraversalDestination(t *testing.T) {
	conf := newWebDAVTestConfig(t)
	if err := conf.CreateDirectories(); err != nil {
		t.Fatalf("failed to create test directories: %v", err)
	}
	r := setupWebDAVRouter(conf)

	// Create source file via PUT
	_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "travsrc2"), 0o700)
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/travsrc2/a.txt", bytes.NewBufferString("A"))
	authBasic(req)
	r.ServeHTTP(w, req)
	assert.InDelta(t, 201, w.Code, 1)

	// Attempt MOVE with traversal in Destination
	w = httptest.NewRecorder()
	req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/travsrc2/a.txt", nil)
	req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/../evil2.txt")
	authBasic(req)
	r.ServeHTTP(w, req)
	if w.Code != http.StatusCreated && w.Code != http.StatusNoContent {
		t.Fatalf("expected success (sanitized) for MOVE, got %d", w.Code)
	}
	// Source removed; destination created inside base, not outside
	assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "travsrc2", "a.txt"))
	parent := filepath.Dir(conf.OriginalsPath())
	assert.NoFileExists(t, filepath.Join(parent, "evil2.txt"))
	assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "evil2.txt"))
}

func TestWebDAVWrite_ReadOnlyForbidden(t *testing.T) {
	conf := config.TestConfig()
	conf.Options().ReadOnly = true
	r := setupWebDAVRouter(conf)
	w := httptest.NewRecorder()
	req := httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/ro", nil)
	authBearer(req)
	r.ServeHTTP(w, req)
	assert.Equal(t, http.StatusForbidden, w.Code)
}
