package services

import (
	"bytes"
	"context"
	"io"
	"log/slog"
	"mime/multipart"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestDeleteTokenVerification(t *testing.T) {
	service := newTestUploadService(t)
	result := createTestBox(t, service, "file.txt", "hello")
	box := getTestBox(t, service, result.BoxID)
	token := tokenFromManageURL(t, result.ManageURL)

	if box.DeleteTokenHash == "" {
		t.Fatalf("DeleteTokenHash was not stored")
	}
	if strings.Contains(box.DeleteTokenHash, token) {
		t.Fatalf("DeleteTokenHash contains the raw token")
	}
	if !service.VerifyDeleteToken(box, token) {
		t.Fatalf("VerifyDeleteToken rejected the correct token")
	}
	if service.VerifyDeleteToken(box, "wrong-token") {
		t.Fatalf("VerifyDeleteToken accepted the wrong token")
	}
}

func TestDeleteBoxWithTokenRemovesMetadataAndFiles(t *testing.T) {
	service := newTestUploadService(t)
	result := createTestBox(t, service, "file.txt", "hello")
	box := getTestBox(t, service, result.BoxID)
	token := tokenFromManageURL(t, result.ManageURL)

	if _, err := os.Stat(filepath.Join(service.filesDir, box.ID)); err != nil {
		t.Fatalf("box files were not created: %v", err)
	}
	if err := service.DeleteBoxWithToken(box.ID, "wrong-token"); err == nil {
		t.Fatalf("DeleteBoxWithToken accepted the wrong token")
	}
	if _, err := service.GetBox(box.ID); err != nil {
		t.Fatalf("box was deleted after wrong token: %v", err)
	}

	if err := service.DeleteBoxWithToken(box.ID, token); err != nil {
		t.Fatalf("DeleteBoxWithToken returned error: %v", err)
	}
	if _, err := service.GetBox(box.ID); !os.IsNotExist(err) {
		t.Fatalf("GetBox after delete error = %v, want os.ErrNotExist", err)
	}
	if _, err := os.Stat(filepath.Join(service.filesDir, box.ID)); !os.IsNotExist(err) {
		t.Fatalf("box directory still exists after delete: %v", err)
	}
}

func TestUserActiveStorageUsedIgnoresExpiredBoxes(t *testing.T) {
	service := newTestUploadService(t)
	active, err := service.CreateBox(testFileHeaders(t, "file", "active.txt", "active"), UploadOptions{MaxDays: 1, OwnerID: "user-1"})
	if err != nil {
		t.Fatalf("CreateBox active returned error: %v", err)
	}
	expired, err := service.CreateBox(testFileHeaders(t, "file", "expired.txt", "expired"), UploadOptions{MaxDays: 1, OwnerID: "user-1"})
	if err != nil {
		t.Fatalf("CreateBox expired returned error: %v", err)
	}
	expiredBox, err := service.GetBox(expired.BoxID)
	if err != nil {
		t.Fatalf("GetBox returned error: %v", err)
	}
	expiredBox.ExpiresAt = time.Now().UTC().Add(-time.Hour)
	if err := service.SaveBox(expiredBox); err != nil {
		t.Fatalf("SaveBox returned error: %v", err)
	}

	activeBox, err := service.GetBox(active.BoxID)
	if err != nil {
		t.Fatalf("GetBox active returned error: %v", err)
	}
	want := activeBox.Files[0].Size
	got, err := service.UserActiveStorageUsed("user-1")
	if err != nil {
		t.Fatalf("UserActiveStorageUsed returned error: %v", err)
	}
	if got != want {
		t.Fatalf("UserActiveStorageUsed = %d, want %d", got, want)
	}
}

func TestLocalStorageBackendAndLegacyFallback(t *testing.T) {
	service := newTestUploadService(t)
	result := createTestBox(t, service, "file.txt", "hello")
	box := getTestBox(t, service, result.BoxID)
	if service.BoxStorageBackendID(box) != StorageBackendLocal {
		t.Fatalf("BoxStorageBackendID = %q", service.BoxStorageBackendID(box))
	}
	if box.Files[0].ObjectKey == "" {
		t.Fatalf("new file did not store object key")
	}
	object, err := service.OpenFileObject(testContext(), box, box.Files[0])
	if err != nil {
		t.Fatalf("OpenFileObject returned error: %v", err)
	}
	data, err := io.ReadAll(object.Body)
	object.Body.Close()
	if err != nil {
		t.Fatalf("ReadAll returned error: %v", err)
	}
	if string(data) != "hello" {
		t.Fatalf("object body = %q", string(data))
	}

	box.StorageBackendID = ""
	box.Files[0].ObjectKey = ""
	object, err = service.OpenFileObject(testContext(), box, box.Files[0])
	if err != nil {
		t.Fatalf("legacy OpenFileObject returned error: %v", err)
	}
	object.Body.Close()
}

func TestContaboStorageConfigAllowsDisplayNamesWithSpaces(t *testing.T) {
	service := newTestUploadService(t)
	cfg, err := service.Storage().CreateS3Backend(StorageBackendConfig{
		Provider:  StorageProviderContabo,
		Name:      "Contabo main",
		Endpoint:  "https://eu2.contabostorage.com",
		Region:    "EU",
		Bucket:    "My Main Bucket",
		AccessKey: "access",
		SecretKey: "secret",
	})
	if err != nil {
		t.Fatalf("CreateS3Backend returned error: %v", err)
	}
	if cfg.Provider != StorageProviderContabo || !cfg.UseSSL || !cfg.PathStyle {
		t.Fatalf("contabo config was not normalized: %+v", cfg)
	}
	if cfg.Bucket != "My Main Bucket" {
		t.Fatalf("bucket = %q", cfg.Bucket)
	}
}

func TestSFTPStorageConfigValidation(t *testing.T) {
	service := newTestUploadService(t)
	cfg, err := service.Storage().CreateS3Backend(StorageBackendConfig{
		Provider:   StorageProviderSFTP,
		Name:       "NAS storage",
		Host:       "files.example.test",
		Username:   "warpbox",
		Password:   "secret",
		RemotePath: "/srv/warpbox//",
	})
	if err != nil {
		t.Fatalf("CreateS3Backend returned error: %v", err)
	}
	if cfg.Type != StorageBackendSFTP || cfg.Provider != StorageProviderSFTP {
		t.Fatalf("sftp config type/provider = %+v", cfg)
	}
	if cfg.Port != 22 {
		t.Fatalf("port = %d, want 22", cfg.Port)
	}
	if cfg.RemotePath != "/srv/warpbox" {
		t.Fatalf("remote path = %q", cfg.RemotePath)
	}
}

func TestStorageUpdateRejectsProviderMutation(t *testing.T) {
	service := newTestUploadService(t)
	cfg, err := service.Storage().CreateBackend(StorageBackendConfig{
		Provider: StorageProviderSFTP,
		Name:     "SFTP",
		Host:     "files.example.test",
		Username: "warpbox",
		Password: "secret",
	})
	if err != nil {
		t.Fatalf("CreateBackend returned error: %v", err)
	}
	if _, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{
		Provider:  StorageProviderS3,
		Name:      "Mutated",
		Endpoint:  "https://s3.example.test",
		Bucket:    "bucket",
		AccessKey: "access",
		SecretKey: "secret",
		UseSSL:    true,
	}); err == nil {
		t.Fatalf("UpdateBackend allowed provider mutation")
	}
	stored, err := service.Storage().BackendConfig(cfg.ID)
	if err != nil {
		t.Fatalf("BackendConfig returned error: %v", err)
	}
	if stored.Provider != StorageProviderSFTP || stored.Type != StorageBackendSFTP {
		t.Fatalf("provider/type mutated despite error: %+v", stored)
	}
	if _, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{
		Provider: StorageProviderSFTP,
		Type:     StorageBackendS3,
		Name:     "Mutated",
		Host:     "files.example.test",
		Username: "warpbox",
		Password: "secret",
	}); err == nil {
		t.Fatalf("UpdateBackend allowed type mutation")
	}
}

func TestStorageUpdatePreservesSecretsWhenBlank(t *testing.T) {
	service := newTestUploadService(t)
	cfg, err := service.Storage().CreateBackend(StorageBackendConfig{
		Provider:   StorageProviderSFTP,
		Name:       "SFTP",
		Host:       "files.example.test",
		Username:   "warpbox",
		Password:   "secret",
		PrivateKey: "private-key",
		HostKey:    "host-key",
	})
	if err != nil {
		t.Fatalf("CreateBackend returned error: %v", err)
	}
	updated, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{
		Provider: StorageProviderSFTP,
		Name:     "SFTP renamed",
		Host:     "files.example.test",
		Username: "warpbox",
	})
	if err != nil {
		t.Fatalf("UpdateBackend returned error: %v", err)
	}
	if updated.Password != "secret" || updated.PrivateKey != "private-key" || updated.HostKey != "host-key" {
		t.Fatalf("blank secret fields were not preserved: %+v", updated)
	}
}

func TestContaboUpdateKeepsTLSAndPathStyleLocked(t *testing.T) {
	service := newTestUploadService(t)
	cfg, err := service.Storage().CreateBackend(StorageBackendConfig{
		Provider:  StorageProviderContabo,
		Name:      "Contabo",
		Endpoint:  "https://eu2.contabostorage.com",
		Bucket:    "My Main Bucket",
		AccessKey: "access",
		SecretKey: "secret",
	})
	if err != nil {
		t.Fatalf("CreateBackend returned error: %v", err)
	}
	updated, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{
		Provider:  StorageProviderContabo,
		Name:      "Contabo",
		Endpoint:  "https://eu2.contabostorage.com",
		Bucket:    "My Main Bucket",
		AccessKey: "access",
		SecretKey: "secret",
		UseSSL:    false,
		PathStyle: false,
	})
	if err != nil {
		t.Fatalf("UpdateBackend returned error: %v", err)
	}
	if !updated.UseSSL || !updated.PathStyle {
		t.Fatalf("contabo TLS/path-style were not locked: %+v", updated)
	}
}

func TestStorageSpeedTestRequiresConnectionAndRuns(t *testing.T) {
	service := newTestUploadService(t)
	if _, err := service.Storage().StartSpeedTest(StorageBackendLocal, StorageSpeedModeSmall); err == nil {
		t.Fatalf("StartSpeedTest allowed speed test before connection test")
	}
	if _, err := service.Storage().TestBackend(StorageBackendLocal); err != nil {
		t.Fatalf("TestBackend local returned error: %v", err)
	}
	test, err := service.Storage().StartSpeedTest(StorageBackendLocal, StorageSpeedModeSmall)
	if err != nil {
		t.Fatalf("StartSpeedTest returned error: %v", err)
	}
	service.Storage().RunSpeedTest(testContext(), test.ID)
	tests, err := service.Storage().ListSpeedTests(StorageBackendLocal, 10)
	if err != nil {
		t.Fatalf("ListSpeedTests returned error: %v", err)
	}
	if len(tests) != 1 {
		t.Fatalf("speed tests len = %d, want 1", len(tests))
	}
	got := tests[0]
	if got.Status != StorageSpeedStatusDone || got.ProgressPercent != 100 || got.Stage != "complete" || got.BytesWritten == 0 || got.BytesRead == 0 || got.FilesWritten == 0 {
		t.Fatalf("speed test did not complete with metrics: %+v", got)
	}
}

func TestCustomStorageSpeedTestUsesRequestedFiles(t *testing.T) {
	service := newTestUploadService(t)
	if _, err := service.Storage().TestBackend(StorageBackendLocal); err != nil {
		t.Fatalf("TestBackend local returned error: %v", err)
	}
	test, err := service.Storage().StartSpeedTestWithOptions(StorageBackendLocal, StorageSpeedTestOptions{
		Mode:             StorageSpeedModeCustom,
		CustomFileCount:  3,
		CustomFileSizeMB: 0.001,
	})
	if err != nil {
		t.Fatalf("StartSpeedTestWithOptions returned error: %v", err)
	}
	service.Storage().RunSpeedTest(testContext(), test.ID)
	tests, err := service.Storage().ListSpeedTests(StorageBackendLocal, 10)
	if err != nil {
		t.Fatalf("ListSpeedTests returned error: %v", err)
	}
	got := tests[0]
	if got.Mode != StorageSpeedModeCustom || got.CustomFileCount != 3 || got.CustomFileSizeMB != 0.001 || got.FilesWritten != 3 || got.Status != StorageSpeedStatusDone {
		t.Fatalf("custom speed test did not use requested files: %+v", got)
	}
}

func TestSMBAndWebDAVStorageConfigValidation(t *testing.T) {
	service := newTestUploadService(t)
	smb, err := service.Storage().CreateS3Backend(StorageBackendConfig{
		Provider:   StorageProviderSMB,
		Name:       "Office NAS",
		Host:       "nas.example.test",
		Username:   "warpbox",
		Password:   "secret",
		Share:      "uploads",
		RemotePath: "/warpbox//",
	})
	if err != nil {
		t.Fatalf("CreateS3Backend smb returned error: %v", err)
	}
	if smb.Type != StorageBackendSMB || smb.Provider != StorageProviderSMB || smb.Port != 445 {
		t.Fatalf("smb config was not normalized: %+v", smb)
	}
	if smb.RemotePath != "/warpbox" {
		t.Fatalf("smb remote path = %q", smb.RemotePath)
	}

	webdav, err := service.Storage().CreateS3Backend(StorageBackendConfig{
		Provider:   StorageProviderWebDAV,
		Name:       "Nextcloud",
		Endpoint:   "https://files.example.test/webdav",
		Username:   "warpbox",
		Password:   "secret",
		RemotePath: "/warpbox",
	})
	if err != nil {
		t.Fatalf("CreateS3Backend webdav returned error: %v", err)
	}
	if webdav.Type != StorageBackendWebDAV || webdav.Provider != StorageProviderWebDAV {
		t.Fatalf("webdav config was not normalized: %+v", webdav)
	}
}

func testContext() context.Context {
	return context.Background()
}

func newTestUploadService(t *testing.T) *UploadService {
	t.Helper()
	service, err := NewUploadService(1024*1024, t.TempDir(), "http://example.test", slog.New(slog.NewTextHandler(io.Discard, nil)))
	if err != nil {
		t.Fatalf("NewUploadService returned error: %v", err)
	}
	t.Cleanup(func() {
		if err := service.Close(); err != nil {
			t.Fatalf("Close returned error: %v", err)
		}
	})
	return service
}

func createTestBox(t *testing.T, service *UploadService, filename, body string) UploadResult {
	t.Helper()
	result, err := service.CreateBox(testFileHeaders(t, "file", filename, body), UploadOptions{MaxDays: 1})
	if err != nil {
		t.Fatalf("CreateBox returned error: %v", err)
	}
	return result
}

func getTestBox(t *testing.T, service *UploadService, boxID string) Box {
	t.Helper()
	box, err := service.GetBox(boxID)
	if err != nil {
		t.Fatalf("GetBox returned error: %v", err)
	}
	return box
}

func testFileHeaders(t *testing.T, field, filename, body string) []*multipart.FileHeader {
	t.Helper()
	var payload bytes.Buffer
	writer := multipart.NewWriter(&payload)
	part, err := writer.CreateFormFile(field, filename)
	if err != nil {
		t.Fatalf("CreateFormFile returned error: %v", err)
	}
	if _, err := part.Write([]byte(body)); err != nil {
		t.Fatalf("part.Write returned error: %v", err)
	}
	if err := writer.Close(); err != nil {
		t.Fatalf("writer.Close returned error: %v", err)
	}

	request := httptest.NewRequest("POST", "/upload", &payload)
	request.Header.Set("Content-Type", writer.FormDataContentType())
	if err := request.ParseMultipartForm(1024 * 1024); err != nil {
		t.Fatalf("ParseMultipartForm returned error: %v", err)
	}
	return request.MultipartForm.File[field]
}

func tokenFromManageURL(t *testing.T, manageURL string) string {
	t.Helper()
	parts := strings.Split(strings.TrimRight(manageURL, "/"), "/")
	if len(parts) == 0 {
		t.Fatalf("empty manage URL")
	}
	return parts[len(parts)-1]
}
