import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('../../services/notificationService', () => ({
  sendTransactionalEmail: vi.fn(),
}))

vi.mock('./review.repo', () => ({
  findMany: vi.fn(),
  findById: vi.fn(),
  updateById: vi.fn(),
  getStats: vi.fn(),
  sendReminder: vi.fn(),
}))

vi.mock('../reservations/reservation.repo', () => ({
  findCompanyWithBrand: vi.fn(),
}))

import { AppError, NotFoundError } from '../../http/errors'
import { sendTransactionalEmail } from '../../services/notificationService'
import * as repo from './review.repo'
import * as reservationRepo from '../reservations/reservation.repo'
import { getReview, listReviews, replyToReview, sendReviewReminder } from './review.service'

function reviewWithReservation(overrides: Record<string, unknown> = {}) {
  return {
    id: 'review_1',
    reservation: {
      id: 'reservation_1',
      reviewPaused: false,
      reviewReminderSentAt: null,
      reviewToken: 'token_123',
      customer: {
        firstName: 'Nora',
        email: 'nora@example.com',
        reviewOptOut: false,
      },
      vehicle: {
        year: 2025,
        make: 'Dacia',
        model: 'Duster',
      },
      ...overrides,
    },
  }
}

describe('review.service', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.setSystemTime(new Date('2026-06-08T12:00:00.000Z'))
    process.env.CARPLACE_URL = 'https://market.example'
  })

  it('returns reviews with rating filters and pagination metadata', async () => {
    vi.mocked(repo.findMany).mockResolvedValue([[{ id: 'review_1', overallRating: 5 }], 8] as never)

    const result = await listReviews('company_1', { rating: 5, page: 2, pageSize: 3 })

    expect(repo.findMany).toHaveBeenCalledWith('company_1', { overallRating: 5 }, 3, 3)
    expect(result).toEqual({
      data: [{ id: 'review_1', overallRating: 5 }],
      meta: { total: 8, page: 2, pageSize: 3, totalPages: 3 },
    })
  })

  it('throws for missing reviews and timestamps company replies on successful replies', async () => {
    vi.mocked(repo.findById).mockResolvedValueOnce(null as never)
    await expect(getReview('missing', 'company_1')).rejects.toBeInstanceOf(NotFoundError)

    vi.mocked(repo.findById).mockResolvedValueOnce({ id: 'review_1' } as never)
    vi.mocked(repo.updateById).mockResolvedValue({ id: 'review_1', companyReply: 'Thanks' } as never)

    const updated = await replyToReview('review_1', 'company_1', 'Thanks')

    expect(repo.updateById).toHaveBeenCalledWith('review_1', {
      companyReply: 'Thanks',
      companyRepliedAt: new Date('2026-06-08T12:00:00.000Z'),
    })
    expect(updated).toEqual({ id: 'review_1', companyReply: 'Thanks' })
  })

  it('sends the first review reminder with localized branding and records the first reminder field', async () => {
    vi.mocked(repo.findById).mockResolvedValue(reviewWithReservation() as never)
    vi.mocked(reservationRepo.findCompanyWithBrand).mockResolvedValue({
      name: 'Rental Co',
      brand: { defaultLocale: 'en', displayName: 'RentalDriveGo' },
    } as never)

    const result = await sendReviewReminder('review_1', 'company_1')

    expect(sendTransactionalEmail).toHaveBeenCalledWith(expect.objectContaining({
      to: 'nora@example.com',
      subject: expect.stringContaining('Dacia Duster'),
      html: expect.stringContaining('https://market.example/review?token=token_123'),
      text: expect.stringContaining('RentalDriveGo'),
    }))
    expect(repo.sendReminder).toHaveBeenCalledWith('reservation_1', new Date('2026-06-08T12:00:00.000Z'), 'reviewReminderSentAt')
    expect(result).toEqual({ success: true, field: 'reviewReminderSentAt' })
  })

  it('uses the final reminder field after the first reminder has already been sent', async () => {
    vi.mocked(repo.findById).mockResolvedValue(reviewWithReservation({
      reviewReminderSentAt: new Date('2026-06-01T00:00:00.000Z'),
    }) as never)
    vi.mocked(reservationRepo.findCompanyWithBrand).mockResolvedValue({ name: 'Rental Co', brand: null } as never)

    const result = await sendReviewReminder('review_1', 'company_1')

    expect(repo.sendReminder).toHaveBeenCalledWith('reservation_1', expect.any(Date), 'reviewFinalReminderSentAt')
    expect(result.field).toBe('reviewFinalReminderSentAt')
  })

  it('blocks review reminders for paused reservations, opted-out customers, missing emails, and missing tokens', async () => {
    vi.mocked(repo.findById)
      .mockResolvedValueOnce(reviewWithReservation({ reviewPaused: true }) as never)
      .mockResolvedValueOnce(reviewWithReservation({ customer: { firstName: 'Nora', email: 'nora@example.com', reviewOptOut: true } }) as never)
      .mockResolvedValueOnce(reviewWithReservation({ customer: { firstName: 'Nora', email: null, reviewOptOut: false } }) as never)
      .mockResolvedValueOnce(reviewWithReservation({ reviewToken: null }) as never)

    await expect(sendReviewReminder('review_1', 'company_1')).rejects.toMatchObject({ error: 'review_paused' } satisfies Partial<AppError>)
    await expect(sendReviewReminder('review_1', 'company_1')).rejects.toMatchObject({ error: 'opted_out' } satisfies Partial<AppError>)
    await expect(sendReviewReminder('review_1', 'company_1')).rejects.toMatchObject({ error: 'no_email' } satisfies Partial<AppError>)
    await expect(sendReviewReminder('review_1', 'company_1')).rejects.toMatchObject({ error: 'no_token' } satisfies Partial<AppError>)

    expect(sendTransactionalEmail).not.toHaveBeenCalled()
  })
})
