import { describe, it, expect, vi, beforeEach } from 'vitest'
import { NotFoundError, ConflictError, AppError } from '../../http/errors'

vi.mock('./carplace.repo', () => ({
  findPublicOffers:              vi.fn(),
  findCitiesFromCompanies:       vi.fn(),
  findListedCompanies:           vi.fn(),
  findPublishedVehicles:         vi.fn(),
  findVehicleForCarplace:     vi.fn(),
  findVehicleForCarplaceById: vi.fn(),
  upsertCarplaceCustomer:     vi.fn(),
  createCarplaceReservation:  vi.fn(),
  findCompanyPage:               vi.fn(),
  findCompanyBySlug:             vi.fn(),
  findCompanyReviews:            vi.fn(),
  findCompanyVehicles:           vi.fn(),
  findVehicleById:               vi.fn(),
  findCompanyOffers:             vi.fn(),
  findReservationByReviewToken:  vi.fn(),
  findReservationForReviewSubmit: vi.fn(),
  createReview:                  vi.fn(),
  invalidateReviewToken:         vi.fn(),
  findOfferByCode:               vi.fn(),
}))

vi.mock('../../services/vehicleAvailabilityService', () => ({
  getVehicleAvailabilitySummary: vi.fn(),
}))

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

vi.mock('../../lib/emailTranslations', () => ({
  carplaceReservationEmail: {
    subject: vi.fn(() => 'Subject'),
    html:    vi.fn(() => '<html>'),
    text:    vi.fn(() => 'text'),
  },
}))

import * as repo from './carplace.repo'
import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService'
import {
  getCities, searchVehicles, createCarplaceQuote, createCarplaceReservation,
  getReviewContext, submitReview, validateOfferCode,
} from './carplace.service'

const SLUG = 'test-company'

function makeVehicle(overrides: object = {}) {
  return {
    id: 'v-1', dailyRate: 10000, year: 2022, make: 'Toyota', model: 'Camry',
    isPublished: true, status: 'AVAILABLE', companyId: 'co-1',
    pickupLocations: ['Casablanca'],
    allowDifferentDropoff: false,
    dropoffLocations: [],
    company: { name: 'Test Co', brand: { displayName: 'Test Co' } },
    ...overrides,
  }
}

beforeEach(() => { vi.clearAllMocks() })

// ────────────────────────────────────────────────────────────────────────────
describe('getCities', () => {
  it('deduplicates and sorts cities case-insensitively', async () => {
    vi.mocked(repo.findCitiesFromCompanies).mockResolvedValue([
      { brand: { publicCity: 'Casablanca' } },
      { brand: { publicCity: 'casablanca' } },
      { brand: { publicCity: 'Rabat' } },
      { brand: null },
    ] as any)

    const result = await getCities()
    expect(result).toEqual(['Casablanca', 'Rabat'])
  })
})

// ────────────────────────────────────────────────────────────────────────────
describe('searchVehicles', () => {
  it('returns published vehicles with an available Carplace status when no dates are selected', async () => {
    vi.mocked(repo.findPublishedVehicles).mockResolvedValue([makeVehicle()] as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })

    const result = await searchVehicles({ city: 'Casablanca' })

    expect(repo.findPublishedVehicles).toHaveBeenCalledOnce()
    expect(result[0].availability).toBe(true)
    expect(result[0].availabilityStatus).toBe('AVAILABLE')
  })

  it('passes the date range to availability and removes unavailable vehicles before pagination', async () => {
    vi.mocked(repo.findPublishedVehicles).mockResolvedValue([makeVehicle()] as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: false, status: 'RESERVED', nextAvailableAt: new Date('2025-07-01'), blockingReason: 'RESERVATION' })

    const result = await searchVehicles({ startDate: '2025-06-01T00:00:00.000Z', endDate: '2025-06-04T00:00:00.000Z' })

    expect(getVehicleAvailabilitySummary).toHaveBeenCalledWith('v-1', {
      companyId: 'co-1',
      range: { startDate: new Date('2025-06-01T00:00:00.000Z'), endDate: new Date('2025-06-04T00:00:00.000Z') },
    })
    expect(result).toEqual([])
  })



  it('paginates after date-specific availability filtering', async () => {
    vi.mocked(repo.findPublishedVehicles).mockResolvedValue([
      makeVehicle({ id: 'v-1' }),
      makeVehicle({ id: 'v-2' }),
      makeVehicle({ id: 'v-3' }),
    ] as any)
    vi.mocked(getVehicleAvailabilitySummary).mockImplementation(async (vehicleId) => ({
      available: vehicleId !== 'v-1',
      status: vehicleId === 'v-1' ? 'RESERVED' : 'AVAILABLE',
      nextAvailableAt: null,
      blockingReason: vehicleId === 'v-1' ? 'RESERVATION' : null,
    }) as any)

    const result = await searchVehicles({
      startDate: '2026-07-10T10:00:00.000Z',
      endDate: '2026-07-12T10:00:00.000Z',
      page: 1,
      pageSize: 1,
    })

    expect(result).toHaveLength(1)
    expect(result[0].id).toBe('v-2')
  })

  it('filters out vehicles that do not support different drop-off when requested', async () => {
    vi.mocked(repo.findPublishedVehicles).mockResolvedValue([
      makeVehicle({ id: 'same-only' }),
      makeVehicle({ id: 'one-way', allowDifferentDropoff: true, dropoffLocations: ['Rabat'] }),
    ] as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })

    const result = await searchVehicles({ pickupLocation: 'Casablanca', dropoffLocation: 'Rabat', dropoffMode: 'different' })

    expect(result).toHaveLength(1)
    expect(result[0].id).toBe('one-way')
  })
})

// ────────────────────────────────────────────────────────────────────────────
describe('createCarplaceQuote', () => {
  const body = {
    vehicleId: 'v-1',
    startDate: '2026-07-10T10:00:00.000Z',
    endDate: '2026-07-13T10:00:00.000Z',
    pickupLocation: 'Casablanca',
    returnLocation: 'Casablanca',
  }

  it('returns an availability-aware three-day estimate', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })

    const result = await createCarplaceQuote(body)

    expect(result.rentalDays).toBe(3)
    expect(result.baseSubtotal).toBe(30000)
    expect(result.estimatedTotal).toBe(30000)
    expect(result.finalPriceRequiresCompanyConfirmation).toBe(true)
  })

  it('calculates fixed and free-day promotions instead of assuming every offer is a percentage', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })
    vi.mocked(repo.findOfferByCode).mockResolvedValue({ companyId: 'co-1', type: 'FREE_DAY', discountValue: 1, title: 'One free day', maxRedemptions: null, redemptionCount: 0 } as any)

    const result = await createCarplaceQuote({ ...body, promoCode: 'FREE1' })

    expect(result.discounts).toEqual([{ label: 'One free day', amount: 10000 }])
    expect(result.estimatedTotal).toBe(20000)
  })

  it('rejects a promotion owned by a different rental company', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })
    vi.mocked(repo.findOfferByCode).mockResolvedValue({ companyId: 'other-company', type: 'PERCENTAGE', discountValue: 10, title: 'Wrong company' } as any)

    await expect(createCarplaceQuote({ ...body, promoCode: 'WRONG' })).rejects.toMatchObject({ error: 'offer_invalid' })
  })
})

// ────────────────────────────────────────────────────────────────────────────
describe('createCarplaceReservation', () => {
  const baseBody = {
    vehicleId: 'v-1', companySlug: SLUG,
    firstName: 'Ali', lastName: 'Ben', email: 'ali@test.com',
    phone: '+212600000000',
    dateOfBirth: '1990-01-01T00:00:00.000Z',
    nationality: 'Moroccan',
    identityDocumentNumber: 'CIN123456',
    fullAddress: '123 Avenue Hassan II, Casablanca',
    driverLicense: 'DL-123456',
    licenseExpiry: '2030-01-01T00:00:00.000Z',
    licenseIssuedAt: '2015-01-01T00:00:00.000Z',
    licenseCountry: 'Morocco',
    licenseCategory: 'B',
    startDate: '2025-06-01T00:00:00.000Z', endDate: '2025-06-04T00:00:00.000Z',
  }

  it('creates reservation and returns reservationId', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })
    vi.mocked(repo.upsertCarplaceCustomer).mockResolvedValue({ id: 'c-1' } as any)
    vi.mocked(repo.createCarplaceReservation).mockResolvedValue({ id: 'r-1', bookingReference: 'BK-2026-ABC123' } as any)

    const result = await createCarplaceReservation({ ...baseBody, pickupLocation: 'Casablanca', returnLocation: 'Casablanca' })
    expect(result.reservationId).toBe('r-1')
    expect(result.bookingReference).toBe('BK-2026-ABC123')
    expect(repo.upsertCarplaceCustomer).toHaveBeenCalledWith('co-1', expect.objectContaining({
      identityDocumentNumber: 'CIN123456',
      fullAddress: '123 Avenue Hassan II, Casablanca',
      driverLicense: 'DL-123456',
    }))
    expect(repo.createCarplaceReservation).toHaveBeenCalledWith(expect.objectContaining({
      pickupLocation: 'Casablanca',
      returnLocation: 'Casablanca',
    }))
  })

  it('throws NotFoundError when vehicle not found', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(null)

    await expect(createCarplaceReservation(baseBody)).rejects.toBeInstanceOf(NotFoundError)
  })

  it('throws AppError when vehicle is unavailable', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: false, status: 'RESERVED', nextAvailableAt: null, blockingReason: 'RESERVATION' })

    await expect(createCarplaceReservation(baseBody)).rejects.toMatchObject({ error: 'unavailable' })
  })

  it('rejects different return locations when the vehicle requires same drop-off', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)

    await expect(
      createCarplaceReservation({ ...baseBody, pickupLocation: 'Casablanca', returnLocation: 'Rabat' }),
    ).rejects.toMatchObject({ error: 'same_dropoff_required' })
  })

  it('stores the same discounted total shown by the Carplace quote', async () => {
    vi.mocked(repo.findVehicleForCarplaceById).mockResolvedValue(makeVehicle() as any)
    vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })
    vi.mocked(repo.findOfferByCode).mockResolvedValue({ companyId: 'co-1', type: 'PERCENTAGE', discountValue: 10, title: 'Ten percent', maxRedemptions: null, redemptionCount: 0 } as any)
    vi.mocked(repo.upsertCarplaceCustomer).mockResolvedValue({ id: 'c-1' } as any)
    vi.mocked(repo.createCarplaceReservation).mockResolvedValue({ id: 'r-2', bookingReference: 'BK-2026-PROMO1' } as any)

    await createCarplaceReservation({ ...baseBody, promoCode: 'SAVE10' })

    expect(repo.createCarplaceReservation).toHaveBeenCalledWith(expect.objectContaining({
      totalAmount: 27000,
    }))
  })

  it('throws AppError for invalid date range', async () => {
    await expect(
      createCarplaceReservation({ ...baseBody, startDate: '2025-06-04T00:00:00.000Z', endDate: '2025-06-01T00:00:00.000Z' }),
    ).rejects.toMatchObject({ error: 'invalid_dates' })
  })
})

// ────────────────────────────────────────────────────────────────────────────
describe('getReviewContext', () => {
  it('returns review context for a valid unreviewd token', async () => {
    vi.mocked(repo.findReservationByReviewToken).mockResolvedValue({
      id: 'r-1', companyId: 'co-1', renterId: null, reviewToken: 'tok',
      vehicle: { year: 2022, make: 'Toyota', model: 'Camry', photos: ['photo.jpg'] },
      company: { name: 'Test Co', brand: { displayName: 'Test Co', logoUrl: 'logo.png' } },
      review: null,
    } as any)

    const result = await getReviewContext('tok')
    expect(result.reservationId).toBe('r-1')
    expect(result.vehiclePhoto).toBe('photo.jpg')
  })

  it('throws NotFoundError for unknown token', async () => {
    vi.mocked(repo.findReservationByReviewToken).mockResolvedValue(null)
    await expect(getReviewContext('bad')).rejects.toBeInstanceOf(NotFoundError)
  })

  it('throws ConflictError when already reviewed', async () => {
    vi.mocked(repo.findReservationByReviewToken).mockResolvedValue({
      id: 'r-1', companyId: 'co-1', renterId: null,
      vehicle: { year: 2022, make: 'Toyota', model: 'Camry', photos: [] },
      company: { name: 'Test Co', brand: null },
      review: { id: 'rev-1' },
    } as any)

    await expect(getReviewContext('tok')).rejects.toBeInstanceOf(ConflictError)
  })
})

// ────────────────────────────────────────────────────────────────────────────
describe('submitReview', () => {
  it('creates review and invalidates token', async () => {
    vi.mocked(repo.findReservationForReviewSubmit).mockResolvedValue({ id: 'r-1', companyId: 'co-1', renterId: null, review: null } as any)
    vi.mocked(repo.createReview).mockResolvedValue({ id: 'rev-1' } as any)
    vi.mocked(repo.invalidateReviewToken).mockResolvedValue(undefined as any)

    const result = await submitReview('tok', { overallRating: 5 })

    expect(repo.createReview).toHaveBeenCalledOnce()
    expect(repo.invalidateReviewToken).toHaveBeenCalledWith('r-1')
    expect(result.reviewId).toBe('rev-1')
  })
})

// ────────────────────────────────────────────────────────────────────────────
describe('validateOfferCode', () => {
  it('returns offer when code is valid and not exhausted', async () => {
    vi.mocked(repo.findOfferByCode).mockResolvedValue({ id: 'o-1', maxRedemptions: null, redemptionCount: 0 } as any)
    const result = await validateOfferCode('PROMO10')
    expect((result as any).id).toBe('o-1')
  })

  it('throws NotFoundError for unknown code', async () => {
    vi.mocked(repo.findOfferByCode).mockResolvedValue(null)
    await expect(validateOfferCode('BAD')).rejects.toBeInstanceOf(NotFoundError)
  })

  it('throws ConflictError when max redemptions reached', async () => {
    vi.mocked(repo.findOfferByCode).mockResolvedValue({ id: 'o-1', maxRedemptions: 10, redemptionCount: 10 } as any)
    await expect(validateOfferCode('PROMO10')).rejects.toBeInstanceOf(ConflictError)
  })
})
