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

/**
 * Integration tests for the Valkey availability mirror
 * (requirement/valkey-online-mirror-plan.md).
 *
 * Real Postgres + real Valkey via test/setup.js — no mocks. We assert on both
 * stores' state after each operation to catch missed mirrors or order bugs.
 */

vi.mock('../../src/plugins/websocket.js', () => ({
  sendToUser: vi.fn(() => false),
  sendToSessionParticipant: vi.fn(() => false),
  registerWebSocketPlugin: vi.fn(),
  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 valkey = await import('../../src/plugins/valkey.js')
const {
  setOnline,
  setOffline,
  heartbeat,
  isMitraReachable,
  recomputeCapacityForMitra,
  recomputeCapacityBySession,
  seedFromPostgres,
  autoOfflineStaleMitras,
  mirrorHeartbeatsToPostgres,
  countAvailableMitrasFromCache,
  invalidateAvailabilityCache,
  VK_MITRAS_ONLINE,
  VK_MITRAS_DEACTIVATED,
  vkCapacityKey,
  vkHeartbeatKey,
} = await import('../../src/services/mitra-status.service.js')
const { updateMitraStatus } = await import('../../src/services/mitra.service.js')
const { findAvailableMitras, acceptPairingRequest, createPairingRequest } = await import('../../src/services/pairing.service.js')
const { createPaymentSession, confirmPaymentSession } = await import('../../src/services/payment.service.js')
const { db, resetDb, resetAppConfig } = await import('../helpers/db.js')
const { getTestValkey } = await import('../helpers/valkey.js')
const { createCustomer, createMitra } = await import('../helpers/fixtures.js')
const { SessionStatus, UserType } = await import('../../src/constants.js')

const v = () => getTestValkey()

describe('mitra-status valkey mirror', () => {
  beforeAll(async () => {
    await resetAppConfig()
  })

  beforeEach(async () => {
    await resetDb()
  })

  // ---------- Seed ----------

  describe('seedFromPostgres', () => {
    it('populates mitras:online from is_online=true rows', async () => {
      const m1 = await createMitra({ callName: 'M1', isOnline: true })
      const m2 = await createMitra({ callName: 'M2', isOnline: true })
      await createMitra({ callName: 'M3', isOnline: false })

      await v().flushdb()
      await seedFromPostgres()

      const members = await v().smembers(VK_MITRAS_ONLINE)
      expect(members.sort()).toEqual([m1.id, m2.id].sort())
    })

    it('seeds mitras:deactivated from is_active=false', async () => {
      const m = await createMitra({ callName: 'Dead', isActive: false })
      await createMitra({ callName: 'Alive', isActive: true })

      await v().flushdb()
      await seedFromPostgres()

      const members = await v().smembers(VK_MITRAS_DEACTIVATED)
      expect(members).toEqual([m.id])
    })

    it('seeds heartbeat keys for online mitras with current timestamp', async () => {
      const m = await createMitra({ callName: 'Live', isOnline: true })
      await v().flushdb()

      const before = Date.now()
      await seedFromPostgres()
      const after = Date.now()

      const ts = await v().get(vkHeartbeatKey(m.id))
      expect(ts).toBeTruthy()
      const seeded = Date.parse(ts)
      expect(seeded).toBeGreaterThanOrEqual(before)
      expect(seeded).toBeLessThanOrEqual(after)
    })

    it('seeds capacity counters from chat_sessions', async () => {
      const c = await createCustomer({ callName: 'C' })
      const m = await createMitra({ callName: 'M', isOnline: true })
      const sql = db()
      await sql`
        INSERT INTO chat_sessions (customer_id, mitra_id, status)
        VALUES (${c.id}, ${m.id}, ${SessionStatus.ACTIVE})
      `

      await v().flushdb()
      await seedFromPostgres()

      expect(await v().get(vkCapacityKey(m.id))).toBe('1')
    })

    it('is idempotent — running twice yields the same state', async () => {
      const m = await createMitra({ callName: 'Idem', isOnline: true })
      await seedFromPostgres()
      const first = {
        online: (await v().smembers(VK_MITRAS_ONLINE)).sort(),
        heartbeat: await v().get(vkHeartbeatKey(m.id)),
      }
      await seedFromPostgres()
      const second = {
        online: (await v().smembers(VK_MITRAS_ONLINE)).sort(),
        heartbeat: await v().get(vkHeartbeatKey(m.id)),
      }
      expect(second.online).toEqual(first.online)
      // Heartbeat is reseeded with NOW each call — must be >= first
      expect(Date.parse(second.heartbeat)).toBeGreaterThanOrEqual(Date.parse(first.heartbeat))
    })
  })

  // ---------- setOnline / setOffline ----------

  describe('setOnline / setOffline write-through', () => {
    it('setOnline adds to mitras:online + writes heartbeat key', async () => {
      const m = await createMitra({ callName: 'Toggle', isOnline: false })
      await v().flushdb()

      await setOnline(m.id)

      expect(await v().sismember(VK_MITRAS_ONLINE, m.id)).toBe(1)
      expect(await v().get(vkHeartbeatKey(m.id))).toBeTruthy()
    })

    it('setOffline removes from mitras:online + deletes heartbeat key', async () => {
      const m = await createMitra({ callName: 'Toggle', isOnline: true })
      await setOnline(m.id) // ensure heartbeat key exists

      await setOffline(m.id)

      expect(await v().sismember(VK_MITRAS_ONLINE, m.id)).toBe(0)
      expect(await v().get(vkHeartbeatKey(m.id))).toBeNull()
    })

    it('setOffline is no-op when mitra was already offline', async () => {
      const m = await createMitra({ callName: 'OffAlready', isOnline: false })
      const sql = db()
      const beforeLogs = await sql`SELECT COUNT(*)::int AS c FROM mitra_online_logs WHERE mitra_id=${m.id}`

      await setOffline(m.id)

      const afterLogs = await sql`SELECT COUNT(*)::int AS c FROM mitra_online_logs WHERE mitra_id=${m.id}`
      expect(afterLogs[0].c).toBe(beforeLogs[0].c)
    })
  })

  // ---------- heartbeat ----------

  describe('heartbeat (Valkey-only)', () => {
    it('writes Valkey timestamp without touching Postgres last_heartbeat_at', async () => {
      const m = await createMitra({ callName: 'Pinger', isOnline: true })
      const sql = db()
      const [before] = await sql`SELECT last_heartbeat_at FROM mitra_online_status WHERE mitra_id=${m.id}`
      const pgBefore = before.last_heartbeat_at

      // Make sure subsequent NOW() would differ
      await new Promise(r => setTimeout(r, 50))
      await heartbeat(m.id)

      const [after] = await sql`SELECT last_heartbeat_at FROM mitra_online_status WHERE mitra_id=${m.id}`
      // Postgres untouched
      expect(after.last_heartbeat_at).toEqual(pgBefore)
      // Valkey updated
      const ts = await v().get(vkHeartbeatKey(m.id))
      expect(ts).toBeTruthy()
      expect(Date.parse(ts)).toBeGreaterThan(pgBefore.getTime())
    })

    it('advances the heartbeat timestamp on each call', async () => {
      const m = await createMitra({ callName: 'P', isOnline: true })
      await heartbeat(m.id)
      const t1 = await v().get(vkHeartbeatKey(m.id))

      await new Promise(r => setTimeout(r, 20))
      await heartbeat(m.id)
      const t2 = await v().get(vkHeartbeatKey(m.id))

      expect(Date.parse(t2)).toBeGreaterThan(Date.parse(t1))
    })
  })

  // ---------- isMitraReachable ----------

  describe('isMitraReachable', () => {
    it('returns true for online mitra with fresh heartbeat', async () => {
      const m = await createMitra({ callName: 'Reach', isOnline: true })
      expect(await isMitraReachable(m.id)).toBe(true)
    })

    it('returns false when mitra is not in mitras:online', async () => {
      const m = await createMitra({ callName: 'NoReach', isOnline: false })
      expect(await isMitraReachable(m.id)).toBe(false)
    })

    it('returns false when heartbeat is stale', async () => {
      const m = await createMitra({ callName: 'Stale', isOnline: true })
      // Force stale heartbeat (one hour ago)
      const ancient = new Date(Date.now() - 3_600_000).toISOString()
      await v().set(vkHeartbeatKey(m.id), ancient)

      expect(await isMitraReachable(m.id)).toBe(false)
    })
  })

  // ---------- recomputeCapacity ----------

  describe('recomputeCapacityForMitra', () => {
    it('counts ACTIVE + PENDING_PAYMENT sessions', async () => {
      const c = await createCustomer({ callName: 'C' })
      const c2 = await createCustomer({ callName: 'C2' })
      const m = await createMitra({ callName: 'Cap', isOnline: true })
      const sql = db()
      await sql`
        INSERT INTO chat_sessions (customer_id, mitra_id, status)
        VALUES (${c.id}, ${m.id}, ${SessionStatus.ACTIVE}),
               (${c2.id}, ${m.id}, ${SessionStatus.PENDING_PAYMENT})
      `
      await recomputeCapacityForMitra(m.id)
      expect(await v().get(vkCapacityKey(m.id))).toBe('2')
    })

    it('excludes ended/closing/extending sessions', async () => {
      const c = await createCustomer({ callName: 'C' })
      const m = await createMitra({ callName: 'Cap', isOnline: true })
      const sql = db()
      await sql`
        INSERT INTO chat_sessions (customer_id, mitra_id, status)
        VALUES (${c.id}, ${m.id}, ${SessionStatus.COMPLETED})
      `
      await recomputeCapacityForMitra(m.id)
      expect(await v().get(vkCapacityKey(m.id))).toBe('0')
    })

    it('no-op when mitraId is null/undefined', async () => {
      await recomputeCapacityForMitra(null)  // should not throw
      await recomputeCapacityForMitra(undefined)
    })
  })

  // ---------- findAvailableMitras ----------

  describe('findAvailableMitras (Valkey-driven)', () => {
    it('returns online + not-deactivated + under-capacity + fresh-heartbeat mitras', async () => {
      const ok = await createMitra({ callName: 'OK', isOnline: true })
      const deact = await createMitra({ callName: 'Deact', isOnline: true, isActive: false })
      const offline = await createMitra({ callName: 'Off', isOnline: false })
      const stale = await createMitra({ callName: 'Stale', isOnline: true })
      await v().set(vkHeartbeatKey(stale.id), new Date(Date.now() - 3_600_000).toISOString())

      const result = await findAvailableMitras()
      const ids = result.map(r => r.id).sort()
      expect(ids).toEqual([ok.id].sort())
      expect(result.find(r => r.id === ok.id).active_session_count).toBe(0)
    })

    it('excludes a mitra whose capacity is at max', async () => {
      const m = await createMitra({ callName: 'AtCap', isOnline: true })
      // max_customers_per_mitra default is 3
      await v().set(vkCapacityKey(m.id), 3)

      const result = await findAvailableMitras()
      expect(result.find(r => r.id === m.id)).toBeUndefined()
    })

    it('returns capacity in the result for the blast loop', async () => {
      const m = await createMitra({ callName: 'WithCap', isOnline: true })
      await v().set(vkCapacityKey(m.id), 2)
      const result = await findAvailableMitras()
      expect(result.find(r => r.id === m.id).active_session_count).toBe(2)
    })
  })

  // ---------- countAvailableMitrasFromCache ----------

  describe('countAvailableMitrasFromCache (beacon)', () => {
    it('caches the snapshot in Valkey with TTL', async () => {
      await createMitra({ callName: 'On', isOnline: true })
      await v().del('availability:snapshot')

      const first = await countAvailableMitrasFromCache()
      expect(first.available).toBe(true)
      expect(first.count).toBe(1)

      const cached = await v().get('availability:snapshot')
      expect(cached).toBeTruthy()
      expect(JSON.parse(cached)).toEqual(first)

      const ttl = await v().ttl('availability:snapshot')
      expect(ttl).toBeGreaterThan(0)
      expect(ttl).toBeLessThanOrEqual(10)
    })

    it('returns cached snapshot on subsequent calls without recompute', async () => {
      await createMitra({ callName: 'On', isOnline: true })
      await countAvailableMitrasFromCache() // primes cache

      // Manually corrupt SET to prove subsequent call reads cache, not Valkey state
      await v().flushdb()
      await v().set('availability:snapshot', JSON.stringify({ available: true, count: 42 }), 'EX', 10)

      const result = await countAvailableMitrasFromCache()
      expect(result.count).toBe(42)
    })

    it('invalidateAvailabilityCache deletes the snapshot', async () => {
      await v().set('availability:snapshot', JSON.stringify({ available: true, count: 1 }), 'EX', 10)
      await invalidateAvailabilityCache()
      expect(await v().get('availability:snapshot')).toBeNull()
    })
  })

  // ---------- autoOfflineStaleMitras ----------

  describe('autoOfflineStaleMitras', () => {
    it('flips Postgres + cleans Valkey for mitras with stale heartbeat', async () => {
      const m = await createMitra({ callName: 'WillStale', isOnline: true })
      const sql = db()

      // Force stale heartbeat
      await v().set(vkHeartbeatKey(m.id), new Date(Date.now() - 3_600_000).toISOString())

      const count = await autoOfflineStaleMitras()
      expect(count).toBe(1)

      const [row] = await sql`SELECT is_online FROM mitra_online_status WHERE mitra_id=${m.id}`
      expect(row.is_online).toBe(false)
      expect(await v().sismember(VK_MITRAS_ONLINE, m.id)).toBe(0)
      expect(await v().get(vkHeartbeatKey(m.id))).toBeNull()

      const [log] = await sql`
        SELECT status FROM mitra_online_logs
        WHERE mitra_id=${m.id} ORDER BY timestamp DESC LIMIT 1
      `
      expect(log.status).toBe('offline')
    })

    it('no-op when no mitras are stale', async () => {
      await createMitra({ callName: 'Fresh', isOnline: true })
      const count = await autoOfflineStaleMitras()
      expect(count).toBe(0)
    })

    it('no-op when require_ping=false', async () => {
      const sql = db()
      await sql`
        UPDATE app_config SET value=${sql.json({ value: false })}
        WHERE key='require_mitra_ping'
      `
      const m = await createMitra({ callName: 'WouldBeStale', isOnline: true })
      await v().set(vkHeartbeatKey(m.id), new Date(Date.now() - 3_600_000).toISOString())

      const count = await autoOfflineStaleMitras()
      expect(count).toBe(0)
      // Restore for other tests
      await sql`
        UPDATE app_config SET value=${sql.json({ value: true })}
        WHERE key='require_mitra_ping'
      `
    })
  })

  // ---------- mirrorHeartbeatsToPostgres ----------

  describe('mirrorHeartbeatsToPostgres', () => {
    it('writes Valkey heartbeat timestamps to Postgres last_heartbeat_at in one batch', async () => {
      const m1 = await createMitra({ callName: 'P1', isOnline: true })
      const m2 = await createMitra({ callName: 'P2', isOnline: true })
      const sql = db()

      const ts = new Date(Date.now() - 2_000).toISOString()
      await v().set(vkHeartbeatKey(m1.id), ts)
      await v().set(vkHeartbeatKey(m2.id), ts)

      const count = await mirrorHeartbeatsToPostgres()
      expect(count).toBe(2)

      const rows = await sql`
        SELECT mitra_id, last_heartbeat_at FROM mitra_online_status
        WHERE mitra_id IN (${m1.id}, ${m2.id})
      `
      for (const row of rows) {
        expect(row.last_heartbeat_at.toISOString()).toBe(ts)
      }
    })

    it('no-op when no mitras are online', async () => {
      await v().del(VK_MITRAS_ONLINE)
      const count = await mirrorHeartbeatsToPostgres()
      expect(count).toBe(0)
    })
  })

  // ---------- updateMitraStatus / revokeAllSessions ----------

  describe('updateMitraStatus + auth_session revocation', () => {
    it('deactivation adds to mitras:deactivated AND revokes all auth_sessions', async () => {
      const m = await createMitra({ callName: 'Banned', isActive: true })
      const sql = db()
      const tokenHash = '$2b$10$abcdefghijklmnopqrstuv'
      await sql`
        INSERT INTO auth_sessions (user_type, user_id, refresh_token_hash, expires_at)
        VALUES (${UserType.MITRA}, ${m.id}, ${tokenHash}, NOW() + INTERVAL '30 days')
      `

      await updateMitraStatus(m.id, false)

      expect(await v().sismember(VK_MITRAS_DEACTIVATED, m.id)).toBe(1)
      const [auth] = await sql`SELECT revoked_at FROM auth_sessions WHERE user_id=${m.id}`
      expect(auth.revoked_at).not.toBeNull()
    })

    it('reactivation removes from mitras:deactivated', async () => {
      const m = await createMitra({ callName: 'Pardoned', isActive: false })
      await v().sadd(VK_MITRAS_DEACTIVATED, m.id)

      await updateMitraStatus(m.id, true)

      expect(await v().sismember(VK_MITRAS_DEACTIVATED, m.id)).toBe(0)
    })
  })

  // ---------- E2E: blast lifecycle ----------

  describe('end-to-end: blast lifecycle drives capacity counter', () => {
    it('mitra accept → capacity++; session end is covered separately', async () => {
      const c = await createCustomer({ callName: 'BlastC' })
      const m = await createMitra({ callName: 'BlastM', isOnline: true })

      const pay = await createPaymentSession({
        customerId: c.id,
        durationMinutes: 15,
        amount: 30000,
      })
      await confirmPaymentSession(pay.id, c.id)
      const session = await createPairingRequest(c.id, { paymentRequestId: pay.id })
      expect(await v().get(vkCapacityKey(m.id))).toBe('0') // no accept yet

      await acceptPairingRequest(session.id, m.id)
      expect(await v().get(vkCapacityKey(m.id))).toBe('1')
    })
  })

  // ---------- Reader fallback when Valkey is unavailable ----------

  describe('reader fallback', () => {
    it('isMitraReachable falls back to Postgres on Valkey error', async () => {
      const m = await createMitra({ callName: 'Fallback', isOnline: true })
      // Stub sismember to throw
      const spy = vi.spyOn(valkey, 'sismember').mockRejectedValue(new Error('valkey down'))
      try {
        // Postgres has is_online=true → fallback returns true
        const result = await isMitraReachable(m.id)
        expect(result).toBe(true)
      } finally {
        spy.mockRestore()
      }
    })

    it('findAvailableMitras falls back to Postgres JOIN when Valkey sdiff throws', async () => {
      const m = await createMitra({ callName: 'FallbackBlast', isOnline: true })
      const spy = vi.spyOn(valkey, 'sdiff').mockRejectedValue(new Error('valkey down'))
      try {
        const result = await findAvailableMitras()
        expect(result.find(r => r.id === m.id)).toBeDefined()
      } finally {
        spy.mockRestore()
      }
    })
  })
})
