package handlers

import (
	"bytes"
	"encoding/json"
	"io"
	"log/slog"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"warpbox.dev/backend/libs/config"
	"warpbox.dev/backend/libs/services"
	"warpbox.dev/backend/libs/web"
)

func TestUploadJSONIncludesManageURLsAndAcceptsShareXField(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	request := multipartUploadRequest(t, "/api/v1/upload", "sharex", "shot.png", "image data")
	request.Header.Set("Accept", "application/json")
	response := httptest.NewRecorder()

	app.Upload(response, request)

	if response.Code != http.StatusCreated {
		t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
	}
	var payload services.UploadResult
	if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
		t.Fatalf("json.Unmarshal returned error: %v", err)
	}
	if payload.BoxURL == "" || payload.ManageURL == "" || payload.DeleteURL == "" {
		t.Fatalf("upload response missing URLs: %+v", payload)
	}
	if !strings.Contains(payload.ManageURL, "/manage/") || !strings.HasSuffix(payload.DeleteURL, "/delete") {
		t.Fatalf("unexpected manage/delete URLs: %+v", payload)
	}
	if len(payload.Files) != 1 || payload.Files[0].Name != "shot.png" {
		t.Fatalf("unexpected files: %+v", payload.Files)
	}
}

func TestUploadTextResponseReturnsOnlyBoxURL(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
	response := httptest.NewRecorder()

	app.Upload(response, request)

	if response.Code != http.StatusCreated {
		t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
	}
	got := response.Body.String()
	if !strings.HasPrefix(got, "http://example.test/d/") || !strings.HasSuffix(got, "\n") {
		t.Fatalf("text response = %q", got)
	}
	if strings.Contains(got, "{") || strings.Contains(got, "manage") {
		t.Fatalf("text response leaked JSON/manage data: %q", got)
	}
}

func TestManageBoxAndDeleteFlow(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	result := uploadThroughApp(t, app)
	token := tokenFromURL(t, result.ManageURL)

	manageRequest := httptest.NewRequest(http.MethodGet, "/d/"+result.BoxID+"/manage/"+token, nil)
	manageRequest.SetPathValue("boxID", result.BoxID)
	manageRequest.SetPathValue("token", token)
	manageResponse := httptest.NewRecorder()
	app.ManageBox(manageResponse, manageRequest)
	if manageResponse.Code != http.StatusOK {
		t.Fatalf("manage status = %d, body = %s", manageResponse.Code, manageResponse.Body.String())
	}
	if !strings.Contains(manageResponse.Body.String(), "Delete upload") {
		t.Fatalf("manage page did not render confirmation")
	}

	deleteRequest := httptest.NewRequest(http.MethodPost, "/d/"+result.BoxID+"/manage/"+token+"/delete", nil)
	deleteRequest.SetPathValue("boxID", result.BoxID)
	deleteRequest.SetPathValue("token", token)
	deleteResponse := httptest.NewRecorder()
	app.ManageDeleteBox(deleteResponse, deleteRequest)
	if deleteResponse.Code != http.StatusSeeOther {
		t.Fatalf("delete status = %d, body = %s", deleteResponse.Code, deleteResponse.Body.String())
	}
	if location := deleteResponse.Header().Get("Location"); location != "/d/"+result.BoxID+"/deleted" {
		t.Fatalf("delete redirect = %q", location)
	}
	if _, err := app.uploadService.GetBox(result.BoxID); !os.IsNotExist(err) {
		t.Fatalf("GetBox after delete error = %v, want os.ErrNotExist", err)
	}
}

func TestManageBoxRejectsInvalidToken(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	result := uploadThroughApp(t, app)

	request := httptest.NewRequest(http.MethodGet, "/d/"+result.BoxID+"/manage/wrong", nil)
	request.SetPathValue("boxID", result.BoxID)
	request.SetPathValue("token", "wrong")
	response := httptest.NewRecorder()

	app.ManageBox(response, request)

	if response.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want %d", response.Code, http.StatusNotFound)
	}
	if strings.Contains(response.Body.String(), result.BoxID) {
		t.Fatalf("invalid token response leaked box id")
	}
}

func TestAPIDocsAndSchemas(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	docsResponse := httptest.NewRecorder()
	app.APIDocs(docsResponse, httptest.NewRequest(http.MethodGet, "/api", nil))
	if docsResponse.Code != http.StatusOK {
		t.Fatalf("docs status = %d, body = %s", docsResponse.Code, docsResponse.Body.String())
	}
	if !strings.Contains(docsResponse.Body.String(), "/api/v1/schemas/upload-request.json") ||
		!strings.Contains(docsResponse.Body.String(), "ShareX setup") {
		t.Fatalf("API docs missing schema links or ShareX tutorial")
	}

	requestSchemaResponse := httptest.NewRecorder()
	app.UploadRequestSchema(requestSchemaResponse, httptest.NewRequest(http.MethodGet, "/api/v1/schemas/upload-request.json", nil))
	if requestSchemaResponse.Code != http.StatusOK {
		t.Fatalf("request schema status = %d", requestSchemaResponse.Code)
	}
	if !strings.Contains(requestSchemaResponse.Body.String(), `"sharex"`) {
		t.Fatalf("request schema missing sharex field")
	}

	responseSchemaResponse := httptest.NewRecorder()
	app.UploadResponseSchema(responseSchemaResponse, httptest.NewRequest(http.MethodGet, "/api/v1/schemas/upload-response.json", nil))
	if responseSchemaResponse.Code != http.StatusOK {
		t.Fatalf("response schema status = %d", responseSchemaResponse.Code)
	}
	if !strings.Contains(responseSchemaResponse.Body.String(), `"manageUrl"`) {
		t.Fatalf("response schema missing manageUrl field")
	}

	shareXResponse := httptest.NewRecorder()
	app.ShareXAnonymousConfig(shareXResponse, httptest.NewRequest(http.MethodGet, "/api/v1/sharex/warpbox-anonymous.sxcu", nil))
	if shareXResponse.Code != http.StatusOK {
		t.Fatalf("ShareX config status = %d", shareXResponse.Code)
	}
	if !strings.Contains(shareXResponse.Body.String(), `"FileFormName":"sharex"`) ||
		!strings.Contains(shareXResponse.Body.String(), `"RequestURL":"http://example.test/api/v1/upload"`) {
		t.Fatalf("ShareX config did not include instance upload settings: %s", shareXResponse.Body.String())
	}
}

func newTestApp(t *testing.T) (*App, func()) {
	t.Helper()
	root := t.TempDir()
	templateDir := filepath.Join(root, "templates")
	if err := copyDir(filepath.Join("..", "..", "templates"), templateDir); err != nil {
		t.Fatalf("copy templates: %v", err)
	}
	staticDir := filepath.Join(root, "static")
	if err := copyDir(filepath.Join("..", "..", "static"), staticDir); err != nil {
		t.Fatalf("copy static: %v", err)
	}

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	cfg := config.Config{
		AppName:       "warpbox.dev",
		AppVersion:    "test",
		BaseURL:       "http://example.test",
		DataDir:       filepath.Join(root, "data"),
		StaticDir:     staticDir,
		TemplateDir:   templateDir,
		MaxUploadSize: 1024 * 1024,
		DefaultSettings: config.SettingsDefaults{
			AnonymousUploadsEnabled: true,
			AnonymousMaxUploadMB:    1,
			AnonymousDailyUploadMB:  8,
			UserDailyUploadMB:       8,
			DefaultUserStorageMB:    16,
			UsageRetentionDays:      30,
		},
	}
	service, err := services.NewUploadService(cfg.MaxUploadSize, cfg.DataDir, cfg.BaseURL, logger)
	if err != nil {
		t.Fatalf("NewUploadService returned error: %v", err)
	}
	renderer, err := web.NewRenderer(cfg.TemplateDir, cfg.AppName, cfg.AppVersion, cfg.BaseURL)
	if err != nil {
		service.Close()
		t.Fatalf("NewRenderer returned error: %v", err)
	}
	authService, err := services.NewAuthService(service.DB(), cfg.BaseURL)
	if err != nil {
		service.Close()
		t.Fatalf("NewAuthService returned error: %v", err)
	}
	settingsService, err := services.NewSettingsService(service.DB(), cfg.DefaultSettings)
	if err != nil {
		service.Close()
		t.Fatalf("NewSettingsService returned error: %v", err)
	}
	return NewApp(cfg, logger, renderer, service, authService, settingsService), func() {
		if err := service.Close(); err != nil {
			t.Fatalf("Close returned error: %v", err)
		}
	}
}

func uploadThroughApp(t *testing.T, app *App) services.UploadResult {
	t.Helper()
	request := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
	request.Header.Set("Accept", "application/json")
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", response.Code, response.Body.String())
	}
	var payload services.UploadResult
	if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
		t.Fatalf("json.Unmarshal returned error: %v", err)
	}
	return payload
}

func multipartUploadRequest(t *testing.T, path, field, filename, body string) *http.Request {
	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(http.MethodPost, path, &payload)
	request.Header.Set("Content-Type", writer.FormDataContentType())
	return request
}

func multipartUploadRequestWithField(t *testing.T, path, field, filename, body, extraName, extraValue string) *http.Request {
	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.WriteField(extraName, extraValue); err != nil {
		t.Fatalf("WriteField returned error: %v", err)
	}
	if err := writer.Close(); err != nil {
		t.Fatalf("writer.Close returned error: %v", err)
	}

	request := httptest.NewRequest(http.MethodPost, path, &payload)
	request.Header.Set("Content-Type", writer.FormDataContentType())
	return request
}

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

func copyDir(src, dst string) error {
	return filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}
		target := filepath.Join(dst, rel)
		if d.IsDir() {
			return os.MkdirAll(target, 0o755)
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		return os.WriteFile(target, data, 0o644)
	})
}
