import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'

// The internal app doesn't pull the WS plugin but does pull notification.service via
// session routes. Mock both for parity with other route tests + safety.
vi.mock('../../../src/plugins/websocket.js', () => ({
  sendToUser: vi.fn(() => false),
  sendToSessionParticipant: vi.fn(() => false),
  registerWebSocketPlugin: vi.fn(async () => {}),
  registerWebSocketRoute: vi.fn(),
  isUserOnlineWs: vi.fn(() => false),
  getSessionConnections: vi.fn(() => ({})),
}))
vi.mock('../../../src/services/notification.service.js', () => ({
  sendPushNotification: vi.fn(async () => true),
  registerDeviceToken: vi.fn(async () => {}),
}))

const { buildInternal } = await import('../../helpers/server.js')
const { resetAppConfig, db } = await import('../../helpers/db.js')
const { createCcUser } = await import('../../helpers/fixtures.js')
const { ccJwt, authHeader } = await import('../../helpers/jwt.js')

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i

/**
 * Stage 3 route tests for the relational pricing CRUD endpoints.
 *
 * We do NOT use resetDb() because:
 *   1. resetDb() doesn't truncate pricing_tiers (deliberate — backfill is expensive).
 *   2. The migration already seeded the canonical chat+call catalog at setup time.
 *
 * Tests that mutate the catalog use sql DELETE/UPDATE inside the test body and clean
 * up in afterEach. Tests that read the catalog rely on the seeded rows.
 */
describe('/internal/config/pricing-tiers', () => {
  let app
  let ccUser
  let token

  beforeAll(async () => {
    await resetAppConfig()
    app = await buildInternal()
    ccUser = await createCcUser({ displayName: 'PricingTierOperator' })
    token = ccJwt(ccUser.id)
  })

  // Track tier UUIDs we POST/seed across ALL tests in this file so afterAll can
  // clean them up without disturbing the canonical seeded catalog. Deliberately
  // NOT reset between tests — every test that creates a row appends and the IDs
  // accumulate; afterAll iterates and DELETEs them at the end.
  const createdTierIds = []

  afterAll(async () => {
    const sql = db()
    if (createdTierIds.length > 0) {
      await sql`DELETE FROM pricing_tiers_history WHERE tier_id = ANY(${createdTierIds})`
      await sql`DELETE FROM pricing_tiers WHERE id = ANY(${createdTierIds})`
    }
    // Defense-in-depth: also wipe any non-canonical chat/call tiers that slipped through.
    // The canonical catalog is the 5 chat + 4 call minutes seeded by migrate.js.
    await sql`
      DELETE FROM pricing_tiers
      WHERE (mode = 'chat' AND minutes NOT IN (5, 12, 30, 60, 120))
         OR (mode = 'call' AND minutes NOT IN (10, 20, 45, 60))
    `
    await app?.close()
  })

  describe('GET /internal/config/pricing-tiers', () => {
    it('returns the canonical chat + call catalog with updated_at on every row', async () => {
      const res = await app.inject({
        method: 'GET',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
      })
      expect(res.statusCode).toBe(200)
      const body = res.json()
      expect(body.success).toBe(true)
      expect(Array.isArray(body.data.chat)).toBe(true)
      expect(Array.isArray(body.data.call)).toBe(true)
      expect(body.data.chat.length).toBeGreaterThan(0)
      expect(body.data.call.length).toBeGreaterThan(0)

      const sample = body.data.chat[0]
      expect(UUID_RE.test(sample.id)).toBe(true)
      expect(typeof sample.minutes).toBe('number')
      expect(typeof sample.price_idr).toBe('number')
      expect('original_price_idr' in sample).toBe(true)  // schema-only, but operator-facing GET exposes it
      expect('updated_at' in sample).toBe(true)
      expect('is_active' in sample).toBe(true)
    })

    it('includes is_active=false tiers (operators must be able to see and re-activate them)', async () => {
      const sql = db()
      // Create a soft-deleted tier and verify it shows up in the internal GET.
      const [row] = await sql`
        INSERT INTO pricing_tiers (mode, minutes, price_idr, sort_order, is_active)
        VALUES ('chat', 777, 77000, 99, false)
        RETURNING id
      `
      createdTierIds.push(row.id)

      const res = await app.inject({
        method: 'GET',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
      })
      const ids = res.json().data.chat.map((t) => t.id)
      expect(ids).toContain(row.id)
      const inactive = res.json().data.chat.find((t) => t.id === row.id)
      expect(inactive.is_active).toBe(false)
    })
  })

  describe('POST /internal/config/pricing-tiers — create', () => {
    it('happy path: creates a tier, writes a history row with change_kind=create, returns 201', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 45, price_idr: 33000, tag: 'test-create', sort_order: 99 },
      })
      expect(res.statusCode).toBe(201)
      const body = res.json()
      expect(body.success).toBe(true)
      expect(UUID_RE.test(body.data.id)).toBe(true)
      expect(body.data.mode).toBe('chat')
      expect(body.data.minutes).toBe(45)
      expect(body.data.price_idr).toBe(33000)
      expect(body.data.tag).toBe('test-create')
      expect(body.data.is_active).toBe(true)
      createdTierIds.push(body.data.id)

      // History row was written in the same transaction.
      const sql = db()
      const history = await sql`
        SELECT change_kind, changed_by, price_idr, minutes, mode
        FROM pricing_tiers_history WHERE tier_id = ${body.data.id}
      `
      expect(history).toHaveLength(1)
      expect(history[0].change_kind).toBe('create')
      expect(history[0].changed_by).toBe(ccUser.id)
      expect(history[0].price_idr).toBe(33000)
    })

    it('422 on duplicate (mode, minutes)', async () => {
      const sql = db()
      const [existing] = await sql`SELECT mode, minutes FROM pricing_tiers WHERE mode = 'chat' AND minutes = 12`
      expect(existing).toBeDefined()

      const res = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 12, price_idr: 99999 },
      })
      expect(res.statusCode).toBe(422)
      expect(res.json().error.code).toBe('VALIDATION')
    })

    it('422 on negative price', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 88, price_idr: -1 },
      })
      expect(res.statusCode).toBe(422)
      expect(res.json().error.field).toBe('price_idr')
    })

    it('422 on non-positive minutes', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 0, price_idr: 1000 },
      })
      expect(res.statusCode).toBe(422)
      expect(res.json().error.field).toBe('minutes')
    })
  })

  describe('PATCH /internal/config/pricing-tiers/:id — update with optimistic-lock', () => {
    it('happy path: updates price_idr and writes update history with correct changed_by', async () => {
      // Create a fresh tier we own so we don't mutate the canonical catalog.
      const createRes = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 91, price_idr: 9100 },
      })
      const created = createRes.json().data
      createdTierIds.push(created.id)

      const patchRes = await app.inject({
        method: 'PATCH',
        url: `/internal/config/pricing-tiers/${created.id}`,
        headers: authHeader(token),
        payload: { updated_at: created.updated_at, price_idr: 9999, tag: 'updated' },
      })
      expect(patchRes.statusCode).toBe(200)
      const updated = patchRes.json().data
      expect(updated.price_idr).toBe(9999)
      expect(updated.tag).toBe('updated')
      expect(new Date(updated.updated_at).getTime()).toBeGreaterThanOrEqual(new Date(created.updated_at).getTime())

      const sql = db()
      const history = await sql`
        SELECT change_kind, changed_by, price_idr
        FROM pricing_tiers_history WHERE tier_id = ${created.id} ORDER BY changed_at ASC
      `
      expect(history).toHaveLength(2)
      expect(history[0].change_kind).toBe('create')
      expect(history[1].change_kind).toBe('update')
      expect(history[1].price_idr).toBe(9999)
      expect(history[1].changed_by).toBe(ccUser.id)
    })

    it('409 STALE_WRITE on mismatched updated_at, with server_updated_at in the error', async () => {
      const createRes = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 92, price_idr: 9200 },
      })
      const created = createRes.json().data
      createdTierIds.push(created.id)

      const stale = new Date(new Date(created.updated_at).getTime() - 60_000).toISOString()
      const patchRes = await app.inject({
        method: 'PATCH',
        url: `/internal/config/pricing-tiers/${created.id}`,
        headers: authHeader(token),
        payload: { updated_at: stale, price_idr: 1 },
      })
      expect(patchRes.statusCode).toBe(409)
      const err = patchRes.json().error
      expect(err.code).toBe('STALE_WRITE')
      expect(err.server_updated_at).toBeDefined()
    })

    it('404 on unknown UUID', async () => {
      const res = await app.inject({
        method: 'PATCH',
        url: '/internal/config/pricing-tiers/00000000-0000-0000-0000-000000000000',
        headers: authHeader(token),
        payload: { updated_at: new Date().toISOString(), price_idr: 1 },
      })
      expect(res.statusCode).toBe(404)
      expect(res.json().error.code).toBe('NOT_FOUND')
    })

    it('422 when id is not a UUID', async () => {
      const res = await app.inject({
        method: 'PATCH',
        url: '/internal/config/pricing-tiers/not-a-uuid',
        headers: authHeader(token),
        payload: { updated_at: new Date().toISOString(), price_idr: 1 },
      })
      expect(res.statusCode).toBe(422)
      expect(res.json().error.field).toBe('id')
    })

    it('422 when updated_at is missing', async () => {
      const createRes = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 93, price_idr: 9300 },
      })
      const created = createRes.json().data
      createdTierIds.push(created.id)

      const res = await app.inject({
        method: 'PATCH',
        url: `/internal/config/pricing-tiers/${created.id}`,
        headers: authHeader(token),
        payload: { price_idr: 1 },
      })
      expect(res.statusCode).toBe(422)
      expect(res.json().error.field).toBe('updated_at')
    })
  })

  describe('DELETE /internal/config/pricing-tiers/:id — soft delete', () => {
    it('flips is_active=false and writes a delete history row', async () => {
      const createRes = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 94, price_idr: 9400 },
      })
      const created = createRes.json().data
      createdTierIds.push(created.id)

      const delRes = await app.inject({
        method: 'DELETE',
        url: `/internal/config/pricing-tiers/${created.id}`,
        headers: authHeader(token),
        payload: { updated_at: created.updated_at },
      })
      expect(delRes.statusCode).toBe(200)
      expect(delRes.json().data.is_active).toBe(false)

      const sql = db()
      const history = await sql`
        SELECT change_kind, is_active, changed_by
        FROM pricing_tiers_history WHERE tier_id = ${created.id} ORDER BY changed_at ASC
      `
      const last = history[history.length - 1]
      expect(last.change_kind).toBe('delete')
      // Per the contract: snapshot reflects POST-state, so is_active=false.
      expect(last.is_active).toBe(false)
      expect(last.changed_by).toBe(ccUser.id)
    })

    it('409 STALE_WRITE on mismatched updated_at', async () => {
      const createRes = await app.inject({
        method: 'POST',
        url: '/internal/config/pricing-tiers',
        headers: authHeader(token),
        payload: { mode: 'chat', minutes: 95, price_idr: 9500 },
      })
      const created = createRes.json().data
      createdTierIds.push(created.id)

      const stale = new Date(new Date(created.updated_at).getTime() - 60_000).toISOString()
      const res = await app.inject({
        method: 'DELETE',
        url: `/internal/config/pricing-tiers/${created.id}`,
        headers: authHeader(token),
        payload: { updated_at: stale },
      })
      expect(res.statusCode).toBe(409)
      expect(res.json().error.code).toBe('STALE_WRITE')
    })

    it('404 on unknown UUID', async () => {
      const res = await app.inject({
        method: 'DELETE',
        url: '/internal/config/pricing-tiers/00000000-0000-0000-0000-000000000000',
        headers: authHeader(token),
        payload: { updated_at: new Date().toISOString() },
      })
      expect(res.statusCode).toBe(404)
    })
  })
})
