package jobs

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/hibiken/asynq"

	"github.com/treytartt/honeydue-api/internal/config"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/repositories"
)

// --- Mock implementations ---

type mockEmailSender struct {
	sendFn func(to, subject, htmlBody, textBody string) error
}

func (m *mockEmailSender) SendEmail(to, subject, htmlBody, textBody string) error {
	if m.sendFn != nil {
		return m.sendFn(to, subject, htmlBody, textBody)
	}
	return nil
}

type mockPushSender struct {
	sendFn func(ctx context.Context, iosTokens, androidTokens []string, title, message string, data map[string]string) error
}

func (m *mockPushSender) SendToAll(ctx context.Context, iosTokens, androidTokens []string, title, message string, data map[string]string) error {
	if m.sendFn != nil {
		return m.sendFn(ctx, iosTokens, androidTokens, title, message, data)
	}
	return nil
}

type mockNotificationRepo struct {
	findPrefsFn     func(userID uint) (*models.NotificationPreference, error)
	getTokensFn     func(userID uint) ([]string, []string, error)
}

func (m *mockNotificationRepo) FindPreferencesByUser(userID uint) (*models.NotificationPreference, error) {
	if m.findPrefsFn != nil {
		return m.findPrefsFn(userID)
	}
	return &models.NotificationPreference{}, nil
}

func (m *mockNotificationRepo) GetActiveTokensForUser(userID uint) ([]string, []string, error) {
	if m.getTokensFn != nil {
		return m.getTokensFn(userID)
	}
	return nil, nil, nil
}

type mockReminderRepo struct {
	cleanupFn func(daysOld int) (int64, error)
	batchFn   func(keys []repositories.ReminderKey) (map[int]bool, error)
	logFn     func(taskID, userID uint, dueDate time.Time, stage models.ReminderStage, notificationID *uint) (*models.TaskReminderLog, error)
}

func (m *mockReminderRepo) HasSentReminderBatch(keys []repositories.ReminderKey) (map[int]bool, error) {
	if m.batchFn != nil {
		return m.batchFn(keys)
	}
	return map[int]bool{}, nil
}

func (m *mockReminderRepo) LogReminder(taskID, userID uint, dueDate time.Time, stage models.ReminderStage, notificationID *uint) (*models.TaskReminderLog, error) {
	if m.logFn != nil {
		return m.logFn(taskID, userID, dueDate, stage, notificationID)
	}
	return &models.TaskReminderLog{}, nil
}

func (m *mockReminderRepo) CleanupOldLogs(daysOld int) (int64, error) {
	if m.cleanupFn != nil {
		return m.cleanupFn(daysOld)
	}
	return 0, nil
}

type mockOnboardingSender struct {
	noResFn   func() (int, error)
	noTasksFn func() (int, error)
}

func (m *mockOnboardingSender) CheckAndSendNoResidenceEmails() (int, error) {
	if m.noResFn != nil {
		return m.noResFn()
	}
	return 0, nil
}

func (m *mockOnboardingSender) CheckAndSendNoTasksEmails() (int, error) {
	if m.noTasksFn != nil {
		return m.noTasksFn()
	}
	return 0, nil
}

type mockNotificationSender struct {
	sendFn func(ctx context.Context, userID uint, notificationType models.NotificationType, task *models.Task) error
}

func (m *mockNotificationSender) CreateAndSendTaskNotification(ctx context.Context, userID uint, notificationType models.NotificationType, task *models.Task) error {
	if m.sendFn != nil {
		return m.sendFn(ctx, userID, notificationType, task)
	}
	return nil
}

// --- Helper to build a handler with mocks ---

func newTestHandler(opts ...func(*Handler)) *Handler {
	h := &Handler{
		config: &config.Config{},
	}
	for _, opt := range opts {
		opt(h)
	}
	return h
}

func makeTask(taskType string, payload interface{}) *asynq.Task {
	data, _ := json.Marshal(payload)
	return asynq.NewTask(taskType, data)
}

// --- HandleSendEmail tests ---

func TestHandleSendEmail_Success(t *testing.T) {
	var called bool
	h := newTestHandler(func(h *Handler) {
		h.emailService = &mockEmailSender{
			sendFn: func(to, subject, htmlBody, textBody string) error {
				called = true
				if to != "test@example.com" {
					t.Errorf("to = %q, want %q", to, "test@example.com")
				}
				return nil
			},
		}
	})

	task := makeTask(TypeSendEmail, EmailPayload{
		To:       "test@example.com",
		Subject:  "Hello",
		HTMLBody: "<b>Hi</b>",
		TextBody: "Hi",
	})

	err := h.HandleSendEmail(context.Background(), task)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !called {
		t.Error("expected email service to be called")
	}
}

func TestHandleSendEmail_InvalidPayload(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.emailService = &mockEmailSender{}
	})

	task := asynq.NewTask(TypeSendEmail, []byte(`{invalid`))
	err := h.HandleSendEmail(context.Background(), task)
	if err == nil {
		t.Error("expected error for invalid payload")
	}
}

func TestHandleSendEmail_SendFails(t *testing.T) {
	sendErr := errors.New("SMTP error")
	h := newTestHandler(func(h *Handler) {
		h.emailService = &mockEmailSender{
			sendFn: func(_, _, _, _ string) error { return sendErr },
		}
	})

	task := makeTask(TypeSendEmail, EmailPayload{To: "a@b.com", Subject: "S"})
	err := h.HandleSendEmail(context.Background(), task)
	if !errors.Is(err, sendErr) {
		t.Errorf("err = %v, want %v", err, sendErr)
	}
}

func TestHandleSendEmail_NilService_Noop(t *testing.T) {
	h := newTestHandler() // emailService is nil

	task := makeTask(TypeSendEmail, EmailPayload{To: "a@b.com", Subject: "S"})
	err := h.HandleSendEmail(context.Background(), task)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

// --- HandleSendPush tests ---

func TestHandleSendPush_Success(t *testing.T) {
	var pushCalled bool
	h := newTestHandler(func(h *Handler) {
		h.notificationRepo = &mockNotificationRepo{
			getTokensFn: func(userID uint) ([]string, []string, error) {
				return []string{"ios-token"}, []string{"android-token"}, nil
			},
		}
		h.pushClient = &mockPushSender{
			sendFn: func(_ context.Context, ios, android []string, title, msg string, data map[string]string) error {
				pushCalled = true
				if len(ios) != 1 || ios[0] != "ios-token" {
					t.Errorf("ios tokens = %v", ios)
				}
				return nil
			},
		}
	})

	task := makeTask(TypeSendPush, PushPayload{
		UserID:  1,
		Title:   "Alert",
		Message: "Hello",
		Data:    map[string]string{"type": "test"},
	})

	err := h.HandleSendPush(context.Background(), task)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !pushCalled {
		t.Error("expected push client to be called")
	}
}

func TestHandleSendPush_InvalidPayload(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.pushClient = &mockPushSender{}
	})

	task := asynq.NewTask(TypeSendPush, []byte(`{bad`))
	err := h.HandleSendPush(context.Background(), task)
	if err == nil {
		t.Error("expected error for invalid payload")
	}
}

func TestHandleSendPush_NoTokens(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.notificationRepo = &mockNotificationRepo{
			getTokensFn: func(userID uint) ([]string, []string, error) {
				return nil, nil, nil // no tokens
			},
		}
		h.pushClient = &mockPushSender{
			sendFn: func(_ context.Context, _, _ []string, _, _ string, _ map[string]string) error {
				t.Error("push should not be called when no tokens")
				return nil
			},
		}
	})

	task := makeTask(TypeSendPush, PushPayload{UserID: 1, Title: "T", Message: "M"})
	err := h.HandleSendPush(context.Background(), task)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestHandleSendPush_NilClient_Noop(t *testing.T) {
	h := newTestHandler() // pushClient is nil

	task := makeTask(TypeSendPush, PushPayload{UserID: 1, Title: "T", Message: "M"})
	err := h.HandleSendPush(context.Background(), task)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

// --- HandleOnboardingEmails tests ---

func TestHandleOnboardingEmails_Disabled(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.config.Features.OnboardingEmailsEnabled = false
	})

	err := h.HandleOnboardingEmails(context.Background(), asynq.NewTask(TypeOnboardingEmails, nil))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestHandleOnboardingEmails_NilService(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.config.Features.OnboardingEmailsEnabled = true
		// onboardingService is nil
	})

	err := h.HandleOnboardingEmails(context.Background(), asynq.NewTask(TypeOnboardingEmails, nil))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestHandleOnboardingEmails_Success(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.config.Features.OnboardingEmailsEnabled = true
		h.onboardingService = &mockOnboardingSender{
			noResFn:   func() (int, error) { return 2, nil },
			noTasksFn: func() (int, error) { return 3, nil },
		}
	})

	err := h.HandleOnboardingEmails(context.Background(), asynq.NewTask(TypeOnboardingEmails, nil))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestHandleOnboardingEmails_BothFail(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.config.Features.OnboardingEmailsEnabled = true
		h.onboardingService = &mockOnboardingSender{
			noResFn:   func() (int, error) { return 0, errors.New("fail1") },
			noTasksFn: func() (int, error) { return 0, errors.New("fail2") },
		}
	})

	err := h.HandleOnboardingEmails(context.Background(), asynq.NewTask(TypeOnboardingEmails, nil))
	if err == nil {
		t.Error("expected error when both sub-tasks fail")
	}
}

func TestHandleOnboardingEmails_PartialFail(t *testing.T) {
	h := newTestHandler(func(h *Handler) {
		h.config.Features.OnboardingEmailsEnabled = true
		h.onboardingService = &mockOnboardingSender{
			noResFn:   func() (int, error) { return 0, errors.New("fail") },
			noTasksFn: func() (int, error) { return 1, nil },
		}
	})

	err := h.HandleOnboardingEmails(context.Background(), asynq.NewTask(TypeOnboardingEmails, nil))
	if err != nil {
		t.Fatalf("partial failure should not return error, got: %v", err)
	}
}

// --- HandleReminderLogCleanup tests ---

func TestHandleReminderLogCleanup_Success(t *testing.T) {
	var calledDays int
	h := newTestHandler(func(h *Handler) {
		h.reminderRepo = &mockReminderRepo{
			cleanupFn: func(daysOld int) (int64, error) {
				calledDays = daysOld
				return 42, nil
			},
		}
	})

	err := h.HandleReminderLogCleanup(context.Background(), asynq.NewTask(TypeReminderLogCleanup, nil))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if calledDays != 90 {
		t.Errorf("cleanup called with %d days, want 90", calledDays)
	}
}

func TestHandleReminderLogCleanup_Error(t *testing.T) {
	cleanupErr := errors.New("db error")
	h := newTestHandler(func(h *Handler) {
		h.reminderRepo = &mockReminderRepo{
			cleanupFn: func(daysOld int) (int64, error) { return 0, cleanupErr },
		}
	})

	err := h.HandleReminderLogCleanup(context.Background(), asynq.NewTask(TypeReminderLogCleanup, nil))
	if !errors.Is(err, cleanupErr) {
		t.Errorf("err = %v, want %v", err, cleanupErr)
	}
}
