package service

import (
	"context"
	"testing"
	"time"

	filemanager "wucher/internal/domain/file_manager"
	helicopterfile "wucher/internal/domain/helicopter_file"
	helicopterfilerealtime "wucher/internal/realtime/helicopterfile"
	"wucher/internal/shared/pkg/uuidv7"
)

type helicopterFileServiceRepoStub struct {
	listPendingFn         func(context.Context, int) ([]helicopterfile.HelicopterFile, error)
	listPendingBySourceFn func(context.Context, []byte, int) ([]helicopterfile.HelicopterFile, error)
	updateFn              func(context.Context, *helicopterfile.HelicopterFile) error
}

func (r *helicopterFileServiceRepoStub) Create(context.Context, *helicopterfile.HelicopterFile) error {
	return nil
}

func (r *helicopterFileServiceRepoStub) Update(ctx context.Context, row *helicopterfile.HelicopterFile) error {
	if r.updateFn != nil {
		return r.updateFn(ctx, row)
	}
	return nil
}

func (r *helicopterFileServiceRepoStub) Delete(context.Context, []byte) error { return nil }

func (r *helicopterFileServiceRepoStub) GetByID(context.Context, []byte) (*helicopterfile.HelicopterFile, error) {
	return nil, nil
}

func (r *helicopterFileServiceRepoStub) List(context.Context, string, string, int, int) ([]helicopterfile.HelicopterFile, int64, error) {
	return nil, 0, nil
}

func (r *helicopterFileServiceRepoStub) ListPendingAttachments(ctx context.Context, limit int) ([]helicopterfile.HelicopterFile, error) {
	if r.listPendingFn != nil {
		return r.listPendingFn(ctx, limit)
	}
	return nil, nil
}

func (r *helicopterFileServiceRepoStub) ListPendingAttachmentsBySourceFileID(ctx context.Context, sourceFileID []byte, limit int) ([]helicopterfile.HelicopterFile, error) {
	if r.listPendingBySourceFn != nil {
		return r.listPendingBySourceFn(ctx, sourceFileID, limit)
	}
	return nil, nil
}

func (r *helicopterFileServiceRepoStub) ListByHelicopter(context.Context, []byte) ([]helicopterfile.HelicopterFile, error) {
	return nil, nil
}

func (r *helicopterFileServiceRepoStub) ListByHelicopterAndSection(context.Context, []byte, string) ([]helicopterfile.HelicopterFile, error) {
	return nil, nil
}

func (r *helicopterFileServiceRepoStub) HelicopterExists(context.Context, []byte) (bool, error) {
	return false, nil
}

func (r *helicopterFileServiceRepoStub) AttachmentExists(context.Context, []byte) (bool, error) {
	return false, nil
}

type helicopterFileServiceManagerStub struct {
	filemanager.Service

	createAttachmentFn   func(context.Context, filemanager.CreateAttachmentParams) (*filemanager.Attachment, error)
	getAttachmentByIDFn  func(context.Context, []byte) (*filemanager.Attachment, error)
	markFileActiveFn     func(context.Context, []byte, []byte) error
	createAttachmentArgs []filemanager.CreateAttachmentParams
	markFileActiveArgs   []struct {
		fileID  []byte
		actorID []byte
	}
}

func (m *helicopterFileServiceManagerStub) CreateAttachment(ctx context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
	copied := params
	copied.FileID = append([]byte(nil), params.FileID...)
	copied.ActorID = append([]byte(nil), params.ActorID...)
	m.createAttachmentArgs = append(m.createAttachmentArgs, copied)
	if m.createAttachmentFn != nil {
		return m.createAttachmentFn(ctx, params)
	}
	return nil, nil
}

func (m *helicopterFileServiceManagerStub) GetAttachmentByID(ctx context.Context, id []byte) (*filemanager.Attachment, error) {
	if m.getAttachmentByIDFn != nil {
		return m.getAttachmentByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *helicopterFileServiceManagerStub) MarkFileActive(ctx context.Context, fileID []byte, actorID []byte) error {
	m.markFileActiveArgs = append(m.markFileActiveArgs, struct {
		fileID  []byte
		actorID []byte
	}{fileID: append([]byte(nil), fileID...), actorID: append([]byte(nil), actorID...)})
	if m.markFileActiveFn != nil {
		return m.markFileActiveFn(ctx, fileID, actorID)
	}
	return nil
}

func (m *helicopterFileServiceManagerStub) ReconcileFileLifecycle(context.Context, []byte, []byte) error {
	return nil
}

func (m *helicopterFileServiceManagerStub) CleanupOrphanPendingFiles(context.Context, time.Time, time.Duration, int, bool) (filemanager.FileCleanupStats, error) {
	return filemanager.FileCleanupStats{}, nil
}

type helicopterFileServiceEventsStub struct {
	publishedUsers []string
	event          *helicopterfilerealtime.Event
}

func (m *helicopterFileServiceEventsStub) PublishToUsers(userUUIDs []string, event helicopterfilerealtime.Event) int {
	m.publishedUsers = append([]string(nil), userUUIDs...)
	copied := event
	m.event = &copied
	return len(userUUIDs)
}

func TestHelicopterFileServiceFinalizePendingAttachmentsPublishesReadyEvent(t *testing.T) {
	actorID := uuidv7.MustBytes()
	helicopterID := uuidv7.MustBytes()
	helicopterFileID := uuidv7.MustBytes()
	sourceFileID := uuidv7.MustBytes()
	attachmentID := uuidv7.MustBytes()

	sourceFile := &filemanager.File{
		ID:       sourceFileID,
		Name:     "purchase_orders 1 (15).xlsx",
		MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
		Status:   filemanager.FileStatusReady,
	}

	repo := &helicopterFileServiceRepoStub{
		listPendingFn: func(_ context.Context, limit int) ([]helicopterfile.HelicopterFile, error) {
			if limit != 1 {
				t.Fatalf("expected limit 1, got %d", limit)
			}
			return []helicopterfile.HelicopterFile{
				{
					ID:               helicopterFileID,
					HelicopterID:     helicopterID,
					SourceFileID:     sourceFileID,
					CreatedBy:        actorID,
					UpdatedBy:        actorID,
					SourceFile:       sourceFile,
					Position:         4,
					IsMandatory:      false,
					Section:          helicopterfile.SectionBeforeFirstFlightInspection,
					UpdatedAt:        time.Unix(100, 0).UTC(),
					CreatedAt:        time.Unix(90, 0).UTC(),
					FileAttachmentID: nil,
				},
			}, nil
		},
		updateFn: func(_ context.Context, row *helicopterfile.HelicopterFile) error {
			if row == nil {
				t.Fatal("expected row to be updated")
			}
			if len(row.FileAttachmentID) != 16 {
				t.Fatalf("expected attachment id to be set, got %d bytes", len(row.FileAttachmentID))
			}
			if len(row.SourceFileID) != 0 {
				t.Fatalf("expected source file id to be cleared, got %d bytes", len(row.SourceFileID))
			}
			return nil
		},
	}

	fm := &helicopterFileServiceManagerStub{
		createAttachmentFn: func(_ context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
			if got, want := uuidStringOrFail(t, params.FileID), uuidStringOrFail(t, sourceFileID); got != want {
				t.Fatalf("unexpected file id: got %s want %s", got, want)
			}
			if got, want := params.RefType, "helicopter"; got != want {
				t.Fatalf("unexpected ref type: got %q want %q", got, want)
			}
			if got, want := params.RefID, uuidStringOrFail(t, helicopterID); got != want {
				t.Fatalf("unexpected ref id: got %q want %q", got, want)
			}
			if params.Category == nil || *params.Category != "helicopter_file" {
				t.Fatalf("expected helicopter_file category, got %#v", params.Category)
			}
			return &filemanager.Attachment{
				ID:     attachmentID,
				FileID: sourceFileID,
				File:   sourceFile,
			}, nil
		},
		getAttachmentByIDFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
			if got, want := uuidStringOrFail(t, id), uuidStringOrFail(t, attachmentID); got != want {
				t.Fatalf("unexpected attachment lookup id: got %s want %s", got, want)
			}
			return &filemanager.Attachment{
				ID:     attachmentID,
				FileID: sourceFileID,
				File:   sourceFile,
			}, nil
		},
	}
	events := &helicopterFileServiceEventsStub{}

	svc := NewHelicopterFileService(repo).
		WithFileManagerService(fm).
		WithHelicopterFileEvents(events)

	finalized, err := svc.FinalizePendingAttachments(context.Background(), 1)
	if err != nil {
		t.Fatalf("FinalizePendingAttachments returned error: %v", err)
	}
	if finalized != 1 {
		t.Fatalf("expected one finalized attachment, got %d", finalized)
	}
	if len(fm.createAttachmentArgs) != 1 {
		t.Fatalf("expected one attachment create call, got %d", len(fm.createAttachmentArgs))
	}
	if len(fm.markFileActiveArgs) != 1 {
		t.Fatalf("expected one mark active call, got %d", len(fm.markFileActiveArgs))
	}
	if events.event == nil {
		t.Fatal("expected helicopter file event to be published")
	}

	expectedUser := uuidStringOrFail(t, actorID)
	if len(events.publishedUsers) != 1 || events.publishedUsers[0] != expectedUser {
		t.Fatalf("unexpected published users: %#v", events.publishedUsers)
	}
	if events.event.EventType != helicopterfilerealtime.DefaultEventType {
		t.Fatalf("expected default event type, got %q", events.event.EventType)
	}
	if events.event.HelicopterID != uuidStringOrFail(t, helicopterID) {
		t.Fatalf("unexpected helicopter id: %+v", events.event)
	}
	if events.event.HelicopterFileID != uuidStringOrFail(t, helicopterFileID) {
		t.Fatalf("unexpected helicopter file id: %+v", events.event)
	}
	if events.event.FileID != uuidStringOrFail(t, attachmentID) {
		t.Fatalf("expected finalized event to point to attachment id, got %+v", events.event)
	}
	if events.event.AttachmentPending {
		t.Fatalf("expected finalized event to mark attachment pending false, got %+v", events.event)
	}
	if events.event.Name != sourceFile.Name || events.event.MimeType != sourceFile.MimeType {
		t.Fatalf("unexpected file metadata in event: %+v", events.event)
	}
}

func TestHelicopterFileServiceFinalizePendingAttachmentsBySourceFileUUIDPublishesReadyEvent(t *testing.T) {
	actorID := uuidv7.MustBytes()
	helicopterID := uuidv7.MustBytes()
	helicopterFileID := uuidv7.MustBytes()
	sourceFileID := uuidv7.MustBytes()
	attachmentID := uuidv7.MustBytes()
	sourceFileUUID := uuidStringOrFail(t, sourceFileID)

	sourceFile := &filemanager.File{
		ID:       sourceFileID,
		Name:     "purchase_orders 1 (15).xlsx",
		MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
		Status:   filemanager.FileStatusReady,
	}

	repo := &helicopterFileServiceRepoStub{
		listPendingBySourceFn: func(_ context.Context, source []byte, limit int) ([]helicopterfile.HelicopterFile, error) {
			if got, want := uuidStringOrFail(t, source), sourceFileUUID; got != want {
				t.Fatalf("unexpected source file id: got %s want %s", got, want)
			}
			if limit != 1 {
				t.Fatalf("expected limit 1, got %d", limit)
			}
			return []helicopterfile.HelicopterFile{
				{
					ID:               helicopterFileID,
					HelicopterID:     helicopterID,
					SourceFileID:     sourceFileID,
					CreatedBy:        actorID,
					UpdatedBy:        actorID,
					SourceFile:       sourceFile,
					Position:         4,
					IsMandatory:      false,
					Section:          helicopterfile.SectionBeforeFirstFlightInspection,
					UpdatedAt:        time.Unix(100, 0).UTC(),
					CreatedAt:        time.Unix(90, 0).UTC(),
					FileAttachmentID: nil,
				},
			}, nil
		},
		updateFn: func(_ context.Context, row *helicopterfile.HelicopterFile) error {
			if row == nil {
				t.Fatal("expected row to be updated")
			}
			if len(row.FileAttachmentID) != 16 {
				t.Fatalf("expected attachment id to be set, got %d bytes", len(row.FileAttachmentID))
			}
			if len(row.SourceFileID) != 0 {
				t.Fatalf("expected source file id to be cleared, got %d bytes", len(row.SourceFileID))
			}
			return nil
		},
	}

	fm := &helicopterFileServiceManagerStub{
		createAttachmentFn: func(_ context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
			if got, want := uuidStringOrFail(t, params.FileID), sourceFileUUID; got != want {
				t.Fatalf("unexpected file id: got %s want %s", got, want)
			}
			return &filemanager.Attachment{
				ID:     attachmentID,
				FileID: sourceFileID,
				File:   sourceFile,
			}, nil
		},
		getAttachmentByIDFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
			if got, want := uuidStringOrFail(t, id), uuidStringOrFail(t, attachmentID); got != want {
				t.Fatalf("unexpected attachment lookup id: got %s want %s", got, want)
			}
			return &filemanager.Attachment{
				ID:     attachmentID,
				FileID: sourceFileID,
				File:   sourceFile,
			}, nil
		},
	}
	events := &helicopterFileServiceEventsStub{}

	svc := NewHelicopterFileService(repo).
		WithFileManagerService(fm).
		WithHelicopterFileEvents(events)

	finalized, err := svc.FinalizePendingAttachmentsBySourceFileUUID(context.Background(), sourceFileUUID, 1)
	if err != nil {
		t.Fatalf("FinalizePendingAttachmentsBySourceFileUUID returned error: %v", err)
	}
	if finalized != 1 {
		t.Fatalf("expected one finalized attachment, got %d", finalized)
	}
	if len(fm.createAttachmentArgs) != 1 {
		t.Fatalf("expected one attachment create call, got %d", len(fm.createAttachmentArgs))
	}
	if len(fm.markFileActiveArgs) != 1 {
		t.Fatalf("expected one mark active call, got %d", len(fm.markFileActiveArgs))
	}
	if events.event == nil {
		t.Fatal("expected helicopter file event to be published")
	}
	if events.event.FileID != uuidStringOrFail(t, attachmentID) {
		t.Fatalf("expected finalized event to point to attachment id, got %+v", events.event)
	}
	if events.event.AttachmentPending {
		t.Fatalf("expected finalized event to mark attachment pending false, got %+v", events.event)
	}
}

func uuidStringOrFail(t *testing.T, raw []byte) string {
	t.Helper()
	out, err := uuidv7.BytesToString(raw)
	if err != nil {
		t.Fatalf("convert uuid: %v", err)
	}
	return out
}
