package handlers

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"

	authdomain "wucher/internal/domain/auth"
	filemanager "wucher/internal/domain/file_manager"
	takeoverdomain "wucher/internal/domain/takeover"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/uuidv7"
	"wucher/internal/transport/http/middleware"
)

type takeoverWOPIStorageMock struct {
	objects map[string]takeoverWOPIStoredObject
}

type takeoverWOPIStoredObject struct {
	payload     []byte
	contentType string
	lastMod     time.Time
	etag        string
	// advertisedSize, when > 0, overrides the SizeBytes reported in object
	// metadata so a test can simulate storage that claims a larger size than the
	// body actually delivers (a truncated read).
	advertisedSize int64
}

func (o takeoverWOPIStoredObject) sizeBytes() int64 {
	if o.advertisedSize > 0 {
		return o.advertisedSize
	}
	return int64(len(o.payload))
}

func (m *takeoverWOPIStorageMock) GetObject(_ context.Context, objectKey string) (io.ReadCloser, filemanager.ObjectMetadata, error) {
	obj, ok := m.objects[strings.TrimSpace(objectKey)]
	if !ok {
		return nil, filemanager.ObjectMetadata{}, filemanager.ErrUploadObjectNotFound
	}
	return io.NopCloser(bytes.NewReader(obj.payload)), filemanager.ObjectMetadata{
		ObjectKey:    objectKey,
		SizeBytes:    obj.sizeBytes(),
		ContentType:  obj.contentType,
		LastModified: &obj.lastMod,
		ETag:         obj.etag,
	}, nil
}

func (m *takeoverWOPIStorageMock) HeadObject(_ context.Context, objectKey string) (filemanager.ObjectMetadata, error) {
	obj, ok := m.objects[strings.TrimSpace(objectKey)]
	if !ok {
		return filemanager.ObjectMetadata{}, filemanager.ErrUploadObjectNotFound
	}
	return filemanager.ObjectMetadata{
		ObjectKey:    objectKey,
		SizeBytes:    obj.sizeBytes(),
		ContentType:  obj.contentType,
		LastModified: &obj.lastMod,
		ETag:         obj.etag,
	}, nil
}

func (m *takeoverWOPIStorageMock) PutObject(_ context.Context, input filemanager.PutObjectInput) (filemanager.ObjectMetadata, error) {
	payload, err := io.ReadAll(input.Body)
	if err != nil {
		return filemanager.ObjectMetadata{}, err
	}
	m.objects[strings.TrimSpace(input.ObjectKey)] = takeoverWOPIStoredObject{
		payload:     append([]byte(nil), payload...),
		contentType: strings.TrimSpace(input.ContentType),
		lastMod:     time.Now().UTC(),
		etag:        "etag-" + strings.TrimSpace(input.ObjectKey),
	}
	obj := m.objects[strings.TrimSpace(input.ObjectKey)]
	return filemanager.ObjectMetadata{
		ObjectKey:    strings.TrimSpace(input.ObjectKey),
		SizeBytes:    int64(len(payload)),
		ContentType:  obj.contentType,
		LastModified: &obj.lastMod,
		ETag:         obj.etag,
	}, nil
}

func (m *takeoverWOPIStorageMock) CopyObject(_ context.Context, sourceObjectKey, destinationObjectKey string) (filemanager.ObjectMetadata, error) {
	src, ok := m.objects[strings.TrimSpace(sourceObjectKey)]
	if !ok {
		return filemanager.ObjectMetadata{}, filemanager.ErrUploadObjectNotFound
	}
	m.objects[strings.TrimSpace(destinationObjectKey)] = src
	obj := m.objects[strings.TrimSpace(destinationObjectKey)]
	return filemanager.ObjectMetadata{
		ObjectKey:    strings.TrimSpace(destinationObjectKey),
		SizeBytes:    int64(len(obj.payload)),
		ContentType:  obj.contentType,
		LastModified: &obj.lastMod,
		ETag:         obj.etag,
	}, nil
}

func (m *takeoverWOPIStorageMock) DeleteObject(_ context.Context, objectKey string) error {
	delete(m.objects, strings.TrimSpace(objectKey))
	return nil
}

func (m *takeoverWOPIStorageMock) PresignGetObject(_ context.Context, objectKey string, _ time.Duration) (string, error) {
	return "https://example.com/" + strings.TrimSpace(objectKey), nil
}

type takeoverWOPIAuthMock struct {
	getUserByIDFn func(context.Context, []byte) (*authdomain.User, error)
}

func (m *takeoverWOPIAuthMock) GetUserByID(ctx context.Context, id []byte) (*authdomain.User, error) {
	if m.getUserByIDFn != nil {
		return m.getUserByIDFn(ctx, id)
	}
	return nil, nil
}

func TestTakeoverWOPIPutContentsReturnsLastModifiedTimeJSON(t *testing.T) {
	actorID := uuidv7.MustBytes()
	fileID := uuidv7.MustBytes()

	fileSvc := &fileManagerTemplateServiceMock{
		getFileByIDAnyFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
			if !bytes.Equal(id, fileID) {
				t.Fatalf("unexpected file id lookup: %x", id)
			}
			return &filemanager.File{
				ID:        fileID,
				Name:      "purchase_orders.xlsx",
				ObjectKey: "files/purchase_orders.xlsx",
				MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				CreatedBy: actorID,
			}, nil
		},
		updateFileFn: func(_ context.Context, row *filemanager.File) error {
			if row == nil || row.ObjectKey != "files/purchase_orders.xlsx" {
				t.Fatalf("unexpected updated row: %#v", row)
			}
			return nil
		},
	}

	xlsxPayload := testXLSXZip(t)
	storage := &takeoverWOPIStorageMock{
		objects: map[string]takeoverWOPIStoredObject{
			"files/purchase_orders.xlsx": {
				payload:     xlsxPayload,
				contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				lastMod:     time.Now().UTC().Add(-time.Minute),
				etag:        "etag-final",
			},
		},
	}

	wopiSvc := service.NewWOPIService(service.WOPIConfig{
		TokenSecret:   "test-secret",
		PublicBaseURL: "https://api.example.com",
		EditActionURL: "https://collabora.example.com/loleaflet/{WOPISrc}",
		TokenTTL:      30 * time.Minute,
	})
	token, _, err := wopiSvc.MintAccessToken(fileID, actorID, nil, service.WOPIPermissionWrite)
	if err != nil {
		t.Fatalf("mint access token: %v", err)
	}

	handler := &TakeoverHandler{
		fileManager: fileSvc,
		storage:     storage,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", actorID)
		return c.Next()
	})
	app.Use(middleware.WOPIRequired(wopiSvc))
	app.Post("/wopi/files/:fileId/contents", handler.PutWOPIContents)

	req := httptest.NewRequest(http.MethodPost, "/wopi/files/"+uuidString(fileID)+"/contents?access_token="+token, bytes.NewReader(xlsxPayload))
	req.Header.Set("Content-Type", "application/octet-stream")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app test failed: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("unexpected status %d", resp.StatusCode)
	}
	var out struct {
		LastModifiedTime string `json:"LastModifiedTime"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if strings.TrimSpace(out.LastModifiedTime) == "" {
		t.Fatal("expected last modified time in response body")
	}
}

func TestTakeoverWOPIPutContentsAllowsSmallTimestampSkew(t *testing.T) {
	actorID := uuidv7.MustBytes()
	fileID := uuidv7.MustBytes()
	now := time.Now().UTC()

	fileSvc := &fileManagerTemplateServiceMock{
		getFileByIDAnyFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
			if !bytes.Equal(id, fileID) {
				t.Fatalf("unexpected file id lookup: %x", id)
			}
			return &filemanager.File{
				ID:        fileID,
				Name:      "purchase_orders.xlsx",
				ObjectKey: "files/purchase_orders.xlsx",
				MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				CreatedBy: actorID,
			}, nil
		},
	}

	xlsxPayload := testXLSXZip(t)
	storage := &takeoverWOPIStorageMock{
		objects: map[string]takeoverWOPIStoredObject{
			"files/purchase_orders.xlsx": {
				payload:     xlsxPayload,
				contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				lastMod:     now,
				etag:        "etag-final",
			},
		},
	}

	wopiSvc := service.NewWOPIService(service.WOPIConfig{
		TokenSecret:   "test-secret",
		PublicBaseURL: "https://api.example.com",
		EditActionURL: "https://collabora.example.com/loleaflet/{WOPISrc}",
		TokenTTL:      30 * time.Minute,
	})
	token, _, err := wopiSvc.MintAccessToken(fileID, actorID, nil, service.WOPIPermissionWrite)
	if err != nil {
		t.Fatalf("mint access token: %v", err)
	}

	handler := &TakeoverHandler{
		fileManager: fileSvc,
		storage:     storage,
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", actorID)
		return c.Next()
	})
	app.Use(middleware.WOPIRequired(wopiSvc))
	app.Post("/wopi/files/:fileId/contents", handler.PutWOPIContents)

	req := httptest.NewRequest(http.MethodPost, "/wopi/files/"+uuidString(fileID)+"/contents?access_token="+token, bytes.NewReader(xlsxPayload))
	req.Header.Set("Content-Type", "application/octet-stream")
	req.Header.Set("X-COOL-WOPI-Timestamp", strconv.FormatInt(now.Add(-5*time.Second).UnixMilli(), 10))
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app test failed: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("unexpected status %d", resp.StatusCode)
	}
}

func TestTakeoverWOPIPutContentsAllowsAdminTakeoverAccess(t *testing.T) {
	adminID := uuidv7.MustBytes()
	fileID := uuidv7.MustBytes()
	takeoverID := uuidv7.MustBytes()
	otherUserID := uuidv7.MustBytes()

	fileSvc := &fileManagerTemplateServiceMock{
		getFileByIDAnyFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
			if !bytes.Equal(id, fileID) {
				t.Fatalf("unexpected file id lookup: %x", id)
			}
			return &filemanager.File{
				ID:        fileID,
				Name:      "purchase_orders.xlsx",
				ObjectKey: "files/purchase_orders.xlsx",
				MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				CreatedBy: otherUserID,
			}, nil
		},
	}

	xlsxPayload := testXLSXZip(t)
	storage := &takeoverWOPIStorageMock{
		objects: map[string]takeoverWOPIStoredObject{
			"files/purchase_orders.xlsx": {
				payload:     xlsxPayload,
				contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				lastMod:     time.Now().UTC().Add(-time.Minute),
				etag:        "etag-final",
			},
		},
	}

	wopiSvc := service.NewWOPIService(service.WOPIConfig{
		TokenSecret:   "test-secret",
		PublicBaseURL: "https://api.example.com",
		EditActionURL: "https://collabora.example.com/loleaflet/{WOPISrc}",
		TokenTTL:      30 * time.Minute,
	})
	token, _, err := wopiSvc.MintAccessToken(fileID, adminID, takeoverID, service.WOPIPermissionWrite)
	if err != nil {
		t.Fatalf("mint access token: %v", err)
	}

	handler := &TakeoverHandler{
		fileManager: fileSvc,
		storage:     storage,
		auth: &takeoverWOPIAuthMock{
			getUserByIDFn: func(_ context.Context, id []byte) (*authdomain.User, error) {
				if !bytes.Equal(id, adminID) {
					t.Fatalf("unexpected auth lookup id: %x", id)
				}
				return &authdomain.User{
					ID: adminID,
					Roles: []authdomain.Role{
						{Name: "admin"},
					},
				}, nil
			},
		},
	}

	takeoverSvc := &takeoverTemplateServiceMock{
		getByIDFn: func(_ context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) {
			if !bytes.Equal(id, takeoverID) {
				t.Fatalf("unexpected takeover id lookup: %x", id)
			}
			return &takeoverdomain.TakeoverAc{
				ID: takeoverID,
				Files: []takeoverdomain.TakeoverFile{
					{
						FileAttachment: &filemanager.Attachment{
							File: &filemanager.File{
								ID:        fileID,
								Name:      "purchase_orders.xlsx",
								ObjectKey: "files/purchase_orders.xlsx",
								MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
								CreatedBy: otherUserID,
							},
						},
					},
				},
			}, nil
		},
	}
	handler.takeoverAc = service.NewTakeoverModuleService(takeoverSvc)

	app := fiber.New()
	app.Use(middleware.WOPIRequired(wopiSvc))
	app.Post("/wopi/files/:fileId/contents", handler.PutWOPIContents)

	req := httptest.NewRequest(http.MethodPost, "/wopi/files/"+uuidString(fileID)+"/contents?access_token="+token, bytes.NewReader(xlsxPayload))
	req.Header.Set("Content-Type", "application/octet-stream")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app test failed: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("unexpected status %d", resp.StatusCode)
	}
}

func TestTakeoverWOPIGetContentsReadsTakeoverScopedTemplateFile(t *testing.T) {
	actorID := uuidv7.MustBytes()
	fileID := uuidv7.MustBytes()
	takeoverID := uuidv7.MustBytes()
	payload := testXLSXZip(t)

	storage := &takeoverWOPIStorageMock{
		objects: map[string]takeoverWOPIStoredObject{
			"files/purchase_orders.xlsx": {
				payload:     payload,
				contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				lastMod:     time.Now().UTC().Add(-time.Minute),
				etag:        "etag-template-copy",
			},
		},
	}
	wopiSvc := service.NewWOPIService(service.WOPIConfig{
		TokenSecret:   "test-secret",
		PublicBaseURL: "https://api.example.com",
		EditActionURL: "https://collabora.example.com/loleaflet/{WOPISrc}",
		TokenTTL:      30 * time.Minute,
	})
	token, _, err := wopiSvc.MintAccessToken(fileID, actorID, takeoverID, service.WOPIPermissionWrite)
	if err != nil {
		t.Fatalf("mint access token: %v", err)
	}

	takeoverSvc := &takeoverTemplateServiceMock{
		getByIDFn: func(_ context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) {
			if !bytes.Equal(id, takeoverID) {
				t.Fatalf("unexpected takeover id lookup: %x", id)
			}
			return &takeoverdomain.TakeoverAc{
				ID:        takeoverID,
				CreatedBy: actorID,
				Files: []takeoverdomain.TakeoverFile{
					{
						FileAttachment: &filemanager.Attachment{
							File: &filemanager.File{
								ID:        fileID,
								Name:      "purchase_orders.xlsx",
								ObjectKey: "files/purchase_orders.xlsx",
								MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
								SizeBytes: int64(len(payload)),
								CreatedBy: actorID,
								UpdatedAt: time.Now().UTC().Add(-time.Minute),
							},
						},
					},
				},
			}, nil
		},
	}
	handler := &TakeoverHandler{
		storage:    storage,
		takeoverAc: service.NewTakeoverModuleService(takeoverSvc),
	}

	app := fiber.New()
	app.Use(middleware.WOPIRequired(wopiSvc))
	app.Get("/wopi/files/:fileId", handler.CheckWOPIFileInfo)
	app.Get("/wopi/files/:fileId/contents", handler.GetWOPIContents)

	infoReq := httptest.NewRequest(http.MethodGet, "/wopi/files/"+uuidString(fileID)+"?access_token="+token, nil)
	infoResp, err := app.Test(infoReq)
	if err != nil {
		t.Fatalf("check info app test failed: %v", err)
	}
	if infoResp.StatusCode != http.StatusOK {
		t.Fatalf("unexpected check info status %d", infoResp.StatusCode)
	}

	contentReq := httptest.NewRequest(http.MethodGet, "/wopi/files/"+uuidString(fileID)+"/contents?access_token="+token, nil)
	contentResp, err := app.Test(contentReq)
	if err != nil {
		t.Fatalf("get contents app test failed: %v", err)
	}
	if contentResp.StatusCode != http.StatusOK {
		t.Fatalf("unexpected get contents status %d", contentResp.StatusCode)
	}
	got, err := io.ReadAll(contentResp.Body)
	if err != nil {
		t.Fatalf("read contents body: %v", err)
	}
	if !bytes.Equal(got, payload) {
		t.Fatalf("unexpected WOPI contents length got %d want %d", len(got), len(payload))
	}
	if ct := strings.TrimSpace(contentResp.Header.Get("Content-Type")); !strings.Contains(ct, "spreadsheetml.sheet") {
		t.Fatalf("unexpected content type %q", ct)
	}
}

// TestTakeoverWOPIGetContentsRejectsTruncatedBody guards the read-fully-and-
// validate-size convention: when the storage body delivers fewer bytes than the
// object metadata advertises, GetWOPIContents must fail with a non-200 error
// rather than silently sending a corrupt 200 that Collabora would save verbatim.
func TestTakeoverWOPIGetContentsRejectsTruncatedBody(t *testing.T) {
	actorID := uuidv7.MustBytes()
	fileID := uuidv7.MustBytes()
	takeoverID := uuidv7.MustBytes()
	payload := testXLSXZip(t)

	storage := &takeoverWOPIStorageMock{
		objects: map[string]takeoverWOPIStoredObject{
			"files/purchase_orders.xlsx": {
				payload:     payload,
				contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
				lastMod:     time.Now().UTC().Add(-time.Minute),
				etag:        "etag-truncated",
				// Advertise more bytes than the body actually returns.
				advertisedSize: int64(len(payload)) + 4096,
			},
		},
	}
	wopiSvc := service.NewWOPIService(service.WOPIConfig{
		TokenSecret:   "test-secret",
		PublicBaseURL: "https://api.example.com",
		EditActionURL: "https://collabora.example.com/loleaflet/{WOPISrc}",
		TokenTTL:      30 * time.Minute,
	})
	token, _, err := wopiSvc.MintAccessToken(fileID, actorID, takeoverID, service.WOPIPermissionWrite)
	if err != nil {
		t.Fatalf("mint access token: %v", err)
	}

	takeoverSvc := &takeoverTemplateServiceMock{
		getByIDFn: func(_ context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) {
			if !bytes.Equal(id, takeoverID) {
				t.Fatalf("unexpected takeover id lookup: %x", id)
			}
			return &takeoverdomain.TakeoverAc{
				ID:        takeoverID,
				CreatedBy: actorID,
				Files: []takeoverdomain.TakeoverFile{
					{
						FileAttachment: &filemanager.Attachment{
							File: &filemanager.File{
								ID:        fileID,
								Name:      "purchase_orders.xlsx",
								ObjectKey: "files/purchase_orders.xlsx",
								MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
								SizeBytes: int64(len(payload)) + 4096,
								CreatedBy: actorID,
								UpdatedAt: time.Now().UTC().Add(-time.Minute),
							},
						},
					},
				},
			}, nil
		},
	}
	handler := &TakeoverHandler{
		storage:    storage,
		takeoverAc: service.NewTakeoverModuleService(takeoverSvc),
	}

	app := fiber.New()
	app.Use(middleware.WOPIRequired(wopiSvc))
	app.Get("/wopi/files/:fileId/contents", handler.GetWOPIContents)

	contentReq := httptest.NewRequest(http.MethodGet, "/wopi/files/"+uuidString(fileID)+"/contents?access_token="+token, nil)
	contentResp, err := app.Test(contentReq)
	if err != nil {
		t.Fatalf("get contents app test failed: %v", err)
	}
	if contentResp.StatusCode == http.StatusOK {
		body, _ := io.ReadAll(contentResp.Body)
		t.Fatalf("expected non-200 for truncated body, got 200 with %d bytes (would corrupt the document)", len(body))
	}
	if contentResp.StatusCode < 400 {
		t.Fatalf("expected error status >= 400 for truncated body, got %d", contentResp.StatusCode)
	}
}
