import { jest } from '@jest/globals'
import mongoose from 'mongoose'
import { DateTime } from 'luxon'
import { createCipheriv, randomBytes, randomUUID } from 'node:crypto'

// Guarded live integration test for dezky Scheduling against the dockerized
// Stalwart + the running platform-api. It ONLY runs when the JMAP session and
// the API are reachable; otherwise the whole suite is skipped so `pnpm test`
// stays green for anyone without the full stack up. When it does run it seeds a
// throwaway host (domain + mailbox via Stalwart admin JMAP, an app-password
// AES-encrypted with SCHEDULING_CREDENTIAL_KEY, and Host/credential/
// availability/event-type docs in Mongo — mirroring the service), then drives
// the real public API and asserts the booking actually lands on the calendar
// and that concurrent same-slot bookings resolve to exactly one winner.

const API = process.env.SCHED_IT_API ?? 'http://localhost:3001'
const STAL = process.env.SCHED_IT_STALWART ?? 'http://stalwart:8080'
const MONGODB_URI = process.env.MONGODB_URI ?? ''
const ADMIN_PW = process.env.STALWART_ADMIN_PASSWORD ?? ''
const CRED_KEY = process.env.SCHEDULING_CREDENTIAL_KEY ?? ''
const TENANT_SLUG = 'baslund-test'

// Throwaway names — never the seeded demo host "anne".
const DOMAIN = 'sched-it.dezky.local'
const RUN = randomUUID().slice(0, 8)
const HOST_LP = `ithost-${RUN}`
const GUEST_LP = `itguest-${RUN}`
const HOST_EMAIL = `${HOST_LP}@${DOMAIN}`
const GUEST_EMAIL = `${GUEST_LP}@${DOMAIN}`
const HOST_PW = 'H-' + randomUUID()
const GUEST_PW = 'G-' + randomUUID()
const HOST_SLUG = `it-${RUN}`
const ET_SLUG = 'consult'
const TZ = 'Europe/Copenhagen'

const adminAuth = () => 'Basic ' + Buffer.from('admin:' + ADMIN_PW).toString('base64')
const basic = (u: string, p: string) => 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64')

// Probe reachability up-front. Top-level await is fine under jest's ESM VM.
// Any HTTP response (even 401) counts as reachable; only a network/DNS failure
// (e.g. running outside the docker network) marks the stack absent.
async function reachable(url: string, init?: RequestInit): Promise<boolean> {
  try {
    await fetch(url, init)
    return true
  } catch {
    return false
  }
}

const stackUp =
  !!MONGODB_URI &&
  !!ADMIN_PW &&
  !!CRED_KEY &&
  (await reachable(`${STAL}/.well-known/jmap`)) &&
  (await reachable(`${API}/api/v1/public/__probe__/__probe__/__probe__`))

if (!stackUp) {
  // eslint-disable-next-line no-console
  console.warn(
    '[scheduling integration] Stalwart/API/env not reachable — skipping live integration suite.',
  )
}

const describeLive = stackUp ? describe : describe.skip

type JmapResp = { methodResponses?: [string, Record<string, unknown>, string][] }
async function jmap(
  auth: string,
  methodCalls: unknown[],
  using = ['urn:ietf:params:jmap:core', 'urn:stalwart:jmap'],
): Promise<JmapResp> {
  const r = await fetch(`${STAL}/jmap`, {
    method: 'POST',
    headers: { Authorization: auth, 'Content-Type': 'application/json' },
    body: JSON.stringify({ using, methodCalls }),
  })
  return r.json() as Promise<JmapResp>
}
const R = (j: JmapResp, n: string): Record<string, unknown> | null =>
  (j.methodResponses || []).find((x) => x[0] === n)?.[1] ?? null

function seal(plain: string): { encryptedSecret: string; iv: string; authTag: string } {
  const key = Buffer.from(CRED_KEY, 'hex')
  const iv = randomBytes(12)
  const c = createCipheriv('aes-256-gcm', key, iv)
  const enc = Buffer.concat([c.update(plain, 'utf8'), c.final()])
  return {
    encryptedSecret: enc.toString('base64'),
    iv: iv.toString('base64'),
    authTag: c.getAuthTag().toString('base64'),
  }
}

describeLive('scheduling ↔ Stalwart (live integration)', () => {
  jest.setTimeout(60_000)

  let conn: typeof mongoose
  let hostId: mongoose.Types.ObjectId
  const base = `${API}/api/v1/public/${TENANT_SLUG}/${HOST_SLUG}/${ET_SLUG}`

  beforeAll(async () => {
    conn = await mongoose.connect(MONGODB_URI)
    const db = conn.connection.db!
    const oid = () => new mongoose.Types.ObjectId()
    const tenant = await db.collection('tenants').findOne({ slug: TENANT_SLUG })
    if (!tenant) throw new Error(`tenant ${TENANT_SLUG} not found`)
    const tenantId = tenant._id

    // ── Stalwart: domain + host & guest mailboxes ──
    let j = await jmap(adminAuth(), [['x:Domain/query', { filter: { name: DOMAIN } }, 'q']])
    let domainId = (R(j, 'x:Domain/query') as { ids?: string[] } | null)?.ids?.[0]
    if (!domainId) {
      j = await jmap(adminAuth(), [['x:Domain/set', { create: { d: { name: DOMAIN } } }, 's']])
      domainId = ((R(j, 'x:Domain/set') as { created?: { d?: { id?: string } } } | null)?.created?.d?.id) ?? undefined
    }

    const ensureMbx = async (lp: string, pw: string, full: string): Promise<string> => {
      const c = await jmap(adminAuth(), [
        [
          'x:Account/set',
          {
            create: {
              u: {
                '@type': 'User',
                name: lp,
                domainId,
                description: full,
                credentials: { '0': { '@type': 'Password', secret: pw } },
              },
            },
          },
          's',
        ],
      ])
      const id = (R(c, 'x:Account/set') as { created?: { u?: { id?: string } } } | null)?.created?.u?.id
      if (!id) throw new Error(`failed to create mailbox ${lp}: ${JSON.stringify(c)}`)
      return id
    }
    const hostAcct = await ensureMbx(HOST_LP, HOST_PW, 'IT Host')
    await ensureMbx(GUEST_LP, GUEST_PW, 'IT Guest')

    // ── app password (on-behalf) + encrypt ──
    j = await jmap(adminAuth(), [
      [
        'x:AppPassword/set',
        {
          accountId: hostAcct,
          create: {
            ap: { description: `dezky-scheduling-it:${RUN}`, permissions: { '@type': 'Inherit' }, allowedIps: {} },
          },
        },
        's',
      ],
    ])
    const ap = (R(j, 'x:AppPassword/set') as { created?: { ap?: { id?: string; secret?: string } } } | null)?.created?.ap
    if (!ap?.secret) throw new Error('failed to mint app password')
    const sealed = seal(ap.secret)

    // ── host calendar id (as host) ──
    const hostAuth = basic(HOST_EMAIL, HOST_PW)
    const ses = (await (await fetch(`${STAL}/.well-known/jmap`, { headers: { Authorization: hostAuth } })).json()) as {
      primaryAccounts: Record<string, string>
    }
    const calAcct = ses.primaryAccounts['urn:ietf:params:jmap:calendars']
    const cg = (await (
      await fetch(`${STAL}/jmap`, {
        method: 'POST',
        headers: { Authorization: hostAuth, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:calendars'],
          methodCalls: [['Calendar/get', { accountId: calAcct }, 'c']],
        }),
      })
    ).json()) as { methodResponses: [string, { list?: { id?: string }[] }, string][] }
    const calId = cg.methodResponses[0][1].list?.[0]?.id
    if (!calId) throw new Error('host calendar not found')

    // ── insert Host / credential / availability / event-type ──
    const now = new Date()
    hostId = oid()
    const schedId = oid()
    const credId = oid()
    const etId = oid()
    await db.collection('scheduling_hosts').insertOne({
      _id: hostId,
      tenantId,
      authentikUserId: `seed-it-${RUN}`,
      email: HOST_EMAIL,
      displayName: 'IT Host',
      slug: HOST_SLUG,
      timezone: TZ,
      stalwartAccountId: hostAcct,
      defaultCalendarId: calId,
      busyCalendarIds: [calId],
      isActive: true,
      createdAt: now,
      updatedAt: now,
    })
    await db.collection('scheduling_stalwart_credentials').insertOne({
      _id: credId,
      tenantId,
      hostId,
      type: 'app_password',
      ...sealed,
      appPasswordId: ap.id,
      jmapSessionUrl: `${STAL}/.well-known/jmap`,
      lastValidatedAt: now,
      createdAt: now,
      updatedAt: now,
    })
    await db.collection('scheduling_availability_schedules').insertOne({
      _id: schedId,
      tenantId,
      hostId,
      name: 'Working hours',
      timezone: TZ,
      weeklyRules: [1, 2, 3, 4, 5].map((dayOfWeek) => ({
        dayOfWeek,
        intervals: [{ startMinute: 540, endMinute: 1020 }],
      })),
      dateOverrides: [],
      createdAt: now,
      updatedAt: now,
    })
    await db.collection('scheduling_event_types').insertOne({
      _id: etId,
      tenantId,
      hostId,
      slug: ET_SLUG,
      title: 'IT consultation',
      description: 'Throwaway integration-test event type.',
      durationMinutes: 30,
      slotIntervalMinutes: 30,
      bufferBeforeMinutes: 0,
      bufferAfterMinutes: 0,
      minimumNoticeMinutes: 0,
      maximumDaysInFuture: 60,
      availabilityScheduleId: schedId,
      locationType: 'jitsi',
      isActive: true,
      createdAt: now,
      updatedAt: now,
    })
  })

  afterAll(async () => {
    // Tidy the throwaway Mongo docs + Stalwart accounts so reruns stay clean.
    try {
      if (conn?.connection?.db && hostId) {
        const db = conn.connection.db
        await db.collection('scheduling_bookings').deleteMany({ hostId })
        await db.collection('scheduling_slot_locks').deleteMany({ hostId })
        await db.collection('scheduling_event_types').deleteMany({ hostId })
        await db.collection('scheduling_availability_schedules').deleteMany({ hostId })
        await db.collection('scheduling_stalwart_credentials').deleteMany({ hostId })
        await db.collection('scheduling_hosts').deleteOne({ _id: hostId })
      }
      const q = await jmap(adminAuth(), [
        ['x:Account/query', { filter: {} }, 'q'],
        [
          'x:Account/get',
          { '#ids': { resultOf: 'q', name: 'x:Account/query', path: '/ids' }, properties: ['emailAddress'] },
          'g',
        ],
      ])
      const list = (R(q, 'x:Account/get') as { list?: { id: string; emailAddress?: string }[] } | null)?.list ?? []
      const toDestroy = list
        .filter((a) => a.emailAddress === HOST_EMAIL || a.emailAddress === GUEST_EMAIL)
        .map((a) => a.id)
      if (toDestroy.length) await jmap(adminAuth(), [['x:Account/set', { destroy: toDestroy }, 'd']])
    } finally {
      await mongoose.disconnect()
    }
  })

  // Resolve a target weekday window once and reuse it across assertions.
  let from = ''
  let to = ''
  beforeAll(() => {
    let day = DateTime.now().setZone(TZ).plus({ days: 7 }).startOf('day')
    while (day.weekday > 5) day = day.plus({ days: 1 })
    from = day.toUTC().toISO()!
    to = day.plus({ days: 1 }).toUTC().toISO()!
  })

  const fetchSlots = async (): Promise<string[]> => {
    const r = await fetch(
      `${base}/slots?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&timezone=${TZ}`,
    )
    const j = (await r.json()) as { slots?: { startUtc: string }[] }
    return (j.slots ?? []).map((s) => s.startUtc)
  }

  it('serves the seeded event type and offers slots on a working day', async () => {
    const info = (await (await fetch(base)).json()) as { eventType?: { title?: string } }
    expect(info?.eventType?.title).toBe('IT consultation')
    const slots = await fetchSlots()
    expect(slots.length).toBeGreaterThan(0)
  })

  it('writes a busy calendar event on booking and removes the slot from availability', async () => {
    const slots = await fetchSlots()
    expect(slots.length).toBeGreaterThan(0)
    const target = slots[0]

    const bk = await fetch(`${base}/bookings`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        startUtc: target,
        attendeeName: 'IT Guest',
        attendeeEmail: GUEST_EMAIL,
        attendeeTimezone: TZ,
      }),
    })
    expect([200, 201]).toContain(bk.status)

    // The host's calendar free/busy must now report the booked slot as busy.
    const hostAuth = basic(HOST_EMAIL, HOST_PW)
    const ses = (await (await fetch(`${STAL}/.well-known/jmap`, { headers: { Authorization: hostAuth } })).json()) as {
      primaryAccounts: Record<string, string>
    }
    const calAcct = ses.primaryAccounts['urn:ietf:params:jmap:calendars']
    const av = (await (
      await fetch(`${STAL}/jmap`, {
        method: 'POST',
        headers: { Authorization: hostAuth, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          using: [
            'urn:ietf:params:jmap:core',
            'urn:ietf:params:jmap:principals',
            'urn:ietf:params:jmap:principals:availability',
          ],
          methodCalls: [['Principal/getAvailability', { accountId: calAcct, id: calAcct, utcStart: from, utcEnd: to }, 'a']],
        }),
      })
    ).json()) as { methodResponses: [string, { list?: { utcStart: string }[] }, string][] }
    const busy = (av.methodResponses[0][1].list ?? []).map((b) => new Date(b.utcStart).getTime())
    expect(busy).toContain(new Date(target).getTime())

    const after = await fetchSlots()
    expect(after).not.toContain(target)
  })

  it('resolves concurrent same-slot bookings to exactly one winner and one conflict', async () => {
    const slots = await fetchSlots()
    expect(slots.length).toBeGreaterThan(0)
    const target = slots[slots.length - 1]

    const statuses = await Promise.all(
      [0, 1].map(() =>
        fetch(`${base}/bookings`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            startUtc: target,
            attendeeName: 'Racer',
            attendeeEmail: GUEST_EMAIL,
            attendeeTimezone: TZ,
          }),
        }).then((r) => r.status),
      ),
    )
    const wins = statuses.filter((s) => s === 200 || s === 201).length
    const conflicts = statuses.filter((s) => s === 409).length
    expect(wins).toBe(1)
    expect(conflicts).toBe(1)
  })
})
