/**
 * Task 2: Multi-hotel + rooming assignment procedures
 * Tests: listHotels, createHotel, updateHotel, deleteHotel, listRooming,
 *        assignStay, assignTeamToHotel, unassignStay
 */
import { afterAll, describe, expect, it } from 'vitest'
import { prisma, createCaller } from '../setup'
import {
  createTestUser,
  createTestProgram,
  createTestProject,
  cleanupTestData,
  uid,
} from '../helpers'
import { logisticsRouter } from '../../src/server/routers/logistics'

// ---------------------------------------------------------------------------
// Helper: build a CONFIRMED FinalistConfirmation with N attendees
// ---------------------------------------------------------------------------
async function setupConfirmedTeam(
  programId: string,
  projectTitle: string,
  memberCount: number,
) {
  const project = await createTestProject(programId, {
    title: projectTitle,
    competitionCategory: 'STARTUP',
  })

  const users = await Promise.all(
    Array.from({ length: memberCount }, (_, i) =>
      prisma.user.create({
        data: {
          id: uid('user'),
          email: `member${i}_${uid()}@test.local`,
          name: `Member ${i} of ${projectTitle}`,
          role: 'APPLICANT',
          roles: ['APPLICANT'],
          status: 'ACTIVE',
        },
      }),
    ),
  )

  const confirmation = await prisma.finalistConfirmation.create({
    data: {
      projectId: project.id,
      category: 'STARTUP',
      status: 'CONFIRMED',
      deadline: new Date(Date.now() + 86400000),
      token: `tok_${uid()}`,
      confirmedAt: new Date(),
    },
  })

  const attendees = await Promise.all(
    users.map((u) =>
      prisma.attendingMember.create({
        data: { confirmationId: confirmation.id, userId: u.id, needsVisa: false },
      }),
    ),
  )

  return { project, users, confirmation, attendees }
}

// ---------------------------------------------------------------------------
// Suite
// ---------------------------------------------------------------------------
describe('logistics hotel CRUD + rooming assignment', () => {
  const programIds: string[] = []
  const userIds: string[] = []

  afterAll(async () => {
    for (const programId of programIds) {
      // Clean in dependency order
      await prisma.hotelStay.deleteMany({
        where: { attendingMember: { confirmation: { project: { programId } } } },
      })
      await prisma.attendingMember.deleteMany({
        where: { confirmation: { project: { programId } } },
      })
      await prisma.finalistConfirmation.deleteMany({ where: { project: { programId } } })
      await prisma.hotel.deleteMany({ where: { programId } })
      await cleanupTestData(programId, [])
    }
    if (userIds.length > 0) {
      await prisma.user.deleteMany({ where: { id: { in: userIds } } })
    }
  })

  // ── listHotels ────────────────────────────────────────────────────────────

  it('listHotels returns all hotels for a program with stay counts', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-list-${uid()}` })
    programIds.push(program.id)

    // Create 2 hotels directly
    await prisma.hotel.createMany({
      data: [
        { programId: program.id, name: 'Hotel Alpha', address: '1 Alpha St' },
        { programId: program.id, name: 'Hotel Beta' },
      ],
    })

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })
    const hotels = await caller.listHotels({ programId: program.id })
    expect(hotels).toHaveLength(2)
    // Sorted by name ascending
    expect(hotels[0].name).toBe('Hotel Alpha')
    expect(hotels[1].name).toBe('Hotel Beta')
    // _count.stays present
    expect(hotels[0]._count.stays).toBe(0)
  })

  // ── createHotel ───────────────────────────────────────────────────────────

  it('createHotel creates a hotel and normalizes empty link to null', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-create-${uid()}` })
    programIds.push(program.id)

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })

    const hotel = await caller.createHotel({
      programId: program.id,
      name: 'Hotel Hermitage',
      address: 'Square Beaumarchais, Monaco',
      link: '',
      notes: 'Near the venue',
    })
    expect(hotel.name).toBe('Hotel Hermitage')
    expect(hotel.programId).toBe(program.id)
    expect(hotel.link).toBeNull()
    expect(hotel.notes).toBe('Near the venue')

    // A second hotel can be created for the same program
    const hotel2 = await caller.createHotel({
      programId: program.id,
      name: 'Hotel Fairmont',
      link: 'https://fairmont.com',
    })
    expect(hotel2.name).toBe('Hotel Fairmont')
    expect(hotel2.link).toBe('https://fairmont.com')

    const count = await prisma.hotel.count({ where: { programId: program.id } })
    expect(count).toBe(2)
  })

  // ── updateHotel ───────────────────────────────────────────────────────────

  it('updateHotel changes the hotel fields', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-update-${uid()}` })
    programIds.push(program.id)

    const created = await prisma.hotel.create({
      data: { programId: program.id, name: 'Old Name', address: 'Old Address' },
    })

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })
    const updated = await caller.updateHotel({
      id: created.id,
      name: 'New Name',
      address: 'New Address',
      notes: 'Updated notes',
    })
    expect(updated.id).toBe(created.id)
    expect(updated.name).toBe('New Name')
    expect(updated.notes).toBe('Updated notes')
  })

  // ── deleteHotel ───────────────────────────────────────────────────────────

  it('deleteHotel rejects when hotel has occupants', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-del-occupied-${uid()}` })
    programIds.push(program.id)

    const hotel = await prisma.hotel.create({
      data: { programId: program.id, name: 'Occupied Hotel' },
    })
    const { attendees } = await setupConfirmedTeam(program.id, `OccupiedTeam-${uid()}`, 1)
    // Assign the stay
    await prisma.hotelStay.create({
      data: { attendingMemberId: attendees[0].id, hotelId: hotel.id },
    })

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })
    await expect(caller.deleteHotel({ id: hotel.id })).rejects.toMatchObject({
      code: 'BAD_REQUEST',
      message: expect.stringContaining('Reassign'),
    })
  })

  it('deleteHotel succeeds when hotel has no occupants', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-del-empty-${uid()}` })
    programIds.push(program.id)

    const hotel = await prisma.hotel.create({
      data: { programId: program.id, name: 'Empty Hotel' },
    })

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })
    const result = await caller.deleteHotel({ id: hotel.id })
    expect(result.success).toBe(true)

    const found = await prisma.hotel.findUnique({ where: { id: hotel.id } })
    expect(found).toBeNull()
  })

  // ── assignStay (upsert: create → update) ─────────────────────────────────

  it('assignStay upserts: creates then updates room number', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-assign-${uid()}` })
    programIds.push(program.id)

    const hotel = await prisma.hotel.create({
      data: { programId: program.id, name: 'Stay Hotel' },
    })
    const { attendees } = await setupConfirmedTeam(program.id, `StayTeam-${uid()}`, 1)

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })

    // Create
    const stay1 = await caller.assignStay({
      attendingMemberId: attendees[0].id,
      hotelId: hotel.id,
      roomNumber: '101',
    })
    expect(stay1.hotelId).toBe(hotel.id)
    expect(stay1.roomNumber).toBe('101')

    // Update (upsert same attendee → same row)
    const stay2 = await caller.assignStay({
      attendingMemberId: attendees[0].id,
      hotelId: hotel.id,
      roomNumber: '202',
    })
    expect(stay2.id).toBe(stay1.id)
    expect(stay2.roomNumber).toBe('202')

    // Exactly 1 row
    const count = await prisma.hotelStay.count({
      where: { attendingMemberId: attendees[0].id },
    })
    expect(count).toBe(1)
  })

  it('assignStay rejects when hotel program does not match attendee program', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program1 = await createTestProgram({ name: `hotels-mismatch-p1-${uid()}` })
    const program2 = await createTestProgram({ name: `hotels-mismatch-p2-${uid()}` })
    programIds.push(program1.id, program2.id)

    const hotelInP2 = await prisma.hotel.create({
      data: { programId: program2.id, name: 'Wrong Hotel' },
    })
    const { attendees } = await setupConfirmedTeam(program1.id, `MismatchTeam-${uid()}`, 1)

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })
    await expect(
      caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotelInP2.id }),
    ).rejects.toMatchObject({ code: 'BAD_REQUEST' })
  })

  // ── assignTeamToHotel ─────────────────────────────────────────────────────

  it('assignTeamToHotel assigns all attendees of a 2-person team', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-team-assign-${uid()}` })
    programIds.push(program.id)

    const hotel = await prisma.hotel.create({
      data: { programId: program.id, name: 'Team Hotel' },
    })
    const { confirmation, attendees } = await setupConfirmedTeam(
      program.id,
      `TeamAssign-${uid()}`,
      2,
    )

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })

    await caller.assignTeamToHotel({
      confirmationId: confirmation.id,
      hotelId: hotel.id,
      checkInAt: new Date('2026-06-28T14:00:00Z'),
      checkOutAt: new Date('2026-07-01T12:00:00Z'),
    })

    const stays = await prisma.hotelStay.findMany({
      where: { attendingMember: { confirmationId: confirmation.id } },
    })
    expect(stays).toHaveLength(2)
    for (const s of stays) {
      expect(s.hotelId).toBe(hotel.id)
      expect(s.checkInAt?.toISOString()).toBe('2026-06-28T14:00:00.000Z')
    }
    // roomNumber not set (null by default on create)
    expect(stays[0].roomNumber).toBeNull()

    // Call again (update) — preserve existing roomNumber (here null → still null)
    await caller.assignTeamToHotel({
      confirmationId: confirmation.id,
      hotelId: hotel.id,
    })
    const stays2 = await prisma.hotelStay.findMany({
      where: { attendingMember: { confirmationId: confirmation.id } },
    })
    expect(stays2).toHaveLength(2)

    // Set individual room, then re-run assignTeamToHotel → roomNumber preserved
    await caller.assignStay({
      attendingMemberId: attendees[0].id,
      hotelId: hotel.id,
      roomNumber: '301',
    })
    await caller.assignTeamToHotel({ confirmationId: confirmation.id, hotelId: hotel.id })
    const preserved = await prisma.hotelStay.findUnique({
      where: { attendingMemberId: attendees[0].id },
    })
    expect(preserved?.roomNumber).toBe('301')
  })

  // ── unassignStay ──────────────────────────────────────────────────────────

  it('unassignStay removes the stay (and is a no-op when none exists)', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-unassign-${uid()}` })
    programIds.push(program.id)

    const hotel = await prisma.hotel.create({
      data: { programId: program.id, name: 'Unassign Hotel' },
    })
    const { attendees } = await setupConfirmedTeam(program.id, `UnassignTeam-${uid()}`, 1)

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })

    await caller.assignStay({ attendingMemberId: attendees[0].id, hotelId: hotel.id })
    await caller.unassignStay({ attendingMemberId: attendees[0].id })

    const count = await prisma.hotelStay.count({
      where: { attendingMemberId: attendees[0].id },
    })
    expect(count).toBe(0)

    // No-op: calling again doesn't throw
    await expect(
      caller.unassignStay({ attendingMemberId: attendees[0].id }),
    ).resolves.not.toThrow()
  })

  // ── listRooming ───────────────────────────────────────────────────────────

  it('listRooming returns CONFIRMED attendees with and without stays', async () => {
    const admin = await createTestUser('SUPER_ADMIN')
    userIds.push(admin.id)
    const program = await createTestProgram({ name: `hotels-rooming-${uid()}` })
    programIds.push(program.id)

    const hotel = await prisma.hotel.create({
      data: { programId: program.id, name: 'Rooming Hotel' },
    })
    // One 2-person confirmed team
    const { attendees, project } = await setupConfirmedTeam(
      program.id,
      `RoomingTeam-${uid()}`,
      2,
    )
    // Assign only attendee[0] to a hotel
    await prisma.hotelStay.create({
      data: { attendingMemberId: attendees[0].id, hotelId: hotel.id, roomNumber: '42' },
    })

    // Also create a PENDING confirmation (should be excluded)
    const pendingProject = await createTestProject(program.id, {
      title: `Pending-${uid()}`,
      competitionCategory: 'STARTUP',
    })
    const pendingUser = await prisma.user.create({
      data: {
        id: uid('user'),
        email: `pending_${uid()}@test.local`,
        name: 'Pending User',
        role: 'APPLICANT',
        roles: ['APPLICANT'],
        status: 'ACTIVE',
      },
    })
    userIds.push(pendingUser.id)
    const pendingConf = await prisma.finalistConfirmation.create({
      data: {
        projectId: pendingProject.id,
        category: 'STARTUP',
        status: 'PENDING',
        deadline: new Date(Date.now() + 86400000),
        token: `tok_${uid()}`,
      },
    })
    await prisma.attendingMember.create({
      data: { confirmationId: pendingConf.id, userId: pendingUser.id },
    })

    const caller = createCaller(logisticsRouter, {
      id: admin.id,
      email: admin.email,
      role: 'SUPER_ADMIN',
    })
    const rows = await caller.listRooming({ programId: program.id })
    type RoomingRow = (typeof rows)[number]

    // Only CONFIRMED (2 rows from the one confirmed team)
    expect(rows).toHaveLength(2)

    // All rows belong to the confirmed team's project
    expect(rows.every((r: RoomingRow) => r.projectId === project.id)).toBe(true)

    // Attendee with stay has non-null stay
    const withStay = rows.find((r: RoomingRow) => r.attendingMemberId === attendees[0].id)
    expect(withStay?.stay).not.toBeNull()
    expect(withStay?.stay?.roomNumber).toBe('42')

    // Attendee without stay has null stay
    const withoutStay = rows.find((r: RoomingRow) => r.attendingMemberId === attendees[1].id)
    expect(withoutStay?.stay).toBeNull()
  })
})
