import { describe, it, expect, vi, beforeEach } from "bun:test"

import type { Doc, Id } from "../convex/_generated/dataModel"

const FIXED_NOW = 1_706_071_200_000
const FIVE_MINUTES_MS = 5 * 60 * 1000

type MockDb = {
  get: ReturnType<typeof vi.fn>
  query: ReturnType<typeof vi.fn>
  insert: ReturnType<typeof vi.fn>
  patch: ReturnType<typeof vi.fn>
}

function createMockDb(): MockDb {
  return {
    get: vi.fn(),
    query: vi.fn(() => ({
      withIndex: vi.fn(() => ({
        filter: vi.fn(() => ({
          first: vi.fn(async () => null),
          collect: vi.fn(async () => []),
          take: vi.fn(async () => []),
        })),
        first: vi.fn(async () => null),
        collect: vi.fn(async () => []),
        take: vi.fn(async () => []),
      })),
      filter: vi.fn(() => ({
        first: vi.fn(async () => null),
        collect: vi.fn(async () => []),
      })),
      first: vi.fn(async () => null),
      collect: vi.fn(async () => []),
    })),
    insert: vi.fn(async () => "inserted_id" as Id<"liveChatSessions">),
    patch: vi.fn(async () => {}),
  }
}

function buildUser(overrides: Partial<Doc<"users">> = {}): Doc<"users"> {
  const user: Record<string, unknown> = {
    _id: "user_1" as Id<"users">,
    _creationTime: FIXED_NOW - 100_000,
    tenantId: "tenant-1",
    name: "Agent Test",
    email: "agent@test.com",
    role: "AGENT",
    authId: "auth_1",
    avatarUrl: undefined,
    createdAt: FIXED_NOW - 100_000,
    updatedAt: FIXED_NOW - 50_000,
    isActive: true,
    isInitialAdmin: false,
    companyId: undefined,
    teamsIds: [],
    managingCompaniesIds: [],
    deletedAt: undefined,
  }
  return { ...(user as Doc<"users">), ...overrides }
}

function buildMachine(overrides: Partial<Doc<"machines">> = {}): Doc<"machines"> {
  const machine: Record<string, unknown> = {
    _id: "machine_1" as Id<"machines">,
    _creationTime: FIXED_NOW - 100_000,
    tenantId: "tenant-1",
    companyId: undefined,
    companySlug: undefined,
    authUserId: undefined,
    authEmail: undefined,
    persona: undefined,
    assignedUserId: undefined,
    assignedUserEmail: undefined,
    assignedUserName: undefined,
    assignedUserRole: undefined,
    hostname: "desktop-01",
    osName: "Windows",
    osVersion: "11",
    architecture: "x86_64",
    macAddresses: ["001122334455"],
    serialNumbers: ["SN123"],
    fingerprint: "fingerprint",
    metadata: {},
    lastHeartbeatAt: FIXED_NOW - 1000, // Online
    status: undefined,
    isActive: true,
    createdAt: FIXED_NOW - 10_000,
    updatedAt: FIXED_NOW - 5_000,
    registeredBy: "agent:desktop",
    linkedUserIds: [],
    remoteAccess: null,
  }
  return { ...(machine as Doc<"machines">), ...overrides }
}

function buildTicket(overrides: Partial<Doc<"tickets">> = {}): Doc<"tickets"> {
  const ticket: Record<string, unknown> = {
    _id: "ticket_1" as Id<"tickets">,
    _creationTime: FIXED_NOW - 50_000,
    tenantId: "tenant-1",
    reference: 1001,
    subject: "Test Ticket",
    status: "OPEN",
    priority: "MEDIUM",
    machineId: "machine_1" as Id<"machines">,
    requesterId: "user_2" as Id<"users">,
    assigneeId: undefined,
    companyId: undefined,
    categoryId: undefined,
    slaPolicyId: undefined,
    tags: [],
    chatEnabled: false,
    createdAt: FIXED_NOW - 50_000,
    updatedAt: FIXED_NOW - 25_000,
    customFieldValues: {},
    channel: "HELPDESK",
    visibility: "ALL",
  }
  return { ...(ticket as Doc<"tickets">), ...overrides }
}

function buildSession(overrides: Partial<Doc<"liveChatSessions">> = {}): Doc<"liveChatSessions"> {
  const session: Record<string, unknown> = {
    _id: "session_1" as Id<"liveChatSessions">,
    _creationTime: FIXED_NOW - 10_000,
    tenantId: "tenant-1",
    ticketId: "ticket_1" as Id<"tickets">,
    machineId: "machine_1" as Id<"machines">,
    agentId: "user_1" as Id<"users">,
    agentSnapshot: {
      name: "Agent Test",
      email: "agent@test.com",
      avatarUrl: undefined,
    },
    status: "ACTIVE",
    startedAt: FIXED_NOW - 10_000,
    lastActivityAt: FIXED_NOW - 5_000,
    unreadByMachine: 0,
    unreadByAgent: 0,
    endedAt: undefined,
  }
  return { ...(session as Doc<"liveChatSessions">), ...overrides }
}

describe("liveChat", () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(FIXED_NOW)
  })

  describe("startSession", () => {
    it("deve criar sessao quando agente valido e maquina online", async () => {
      const db = createMockDb()
      const machine = buildMachine({ lastHeartbeatAt: FIXED_NOW - 1000 })
      const agent = buildUser({ role: "AGENT" })
      const ticket = buildTicket({ machineId: machine._id })

      db.get.mockImplementation(async (id: string) => {
        if (id === ticket._id) return ticket
        if (id === agent._id) return agent
        if (id === machine._id) return machine
        return null
      })

      db.query.mockReturnValue({
        withIndex: vi.fn(() => ({
          filter: vi.fn(() => ({
            first: vi.fn(async () => null), // Nenhuma sessao ativa
          })),
        })),
      })

      db.insert.mockResolvedValue("new_session_id" as Id<"liveChatSessions">)

      // Simular chamada da mutation
      const ticketFromDb = await db.get(ticket._id)
      const agentFromDb = await db.get(agent._id)
      const machineFromDb = await db.get(ticket.machineId)

      expect(ticketFromDb).toBeTruthy()
      expect(agentFromDb?.role?.toUpperCase()).toBe("AGENT")
      expect(machineFromDb?.lastHeartbeatAt).toBeGreaterThan(FIXED_NOW - FIVE_MINUTES_MS)
    })

    it("deve falhar quando maquina offline", async () => {
      const offlineMachine = buildMachine({
        lastHeartbeatAt: FIXED_NOW - FIVE_MINUTES_MS - 1000, // Offline
      })

      const isOffline = !offlineMachine.lastHeartbeatAt ||
        offlineMachine.lastHeartbeatAt < FIXED_NOW - FIVE_MINUTES_MS

      expect(isOffline).toBe(true)
    })

    it("deve retornar sessao existente se ja houver uma ativa", async () => {
      const existingSession = buildSession()

      const db = createMockDb()
      db.query.mockReturnValue({
        withIndex: vi.fn(() => ({
          filter: vi.fn(() => ({
            first: vi.fn(async () => existingSession),
          })),
        })),
      })

      const queryResult = db.query("liveChatSessions")
      const withIndexResult = queryResult.withIndex("by_ticket", vi.fn())
      const filterResult = withIndexResult.filter(vi.fn())
      const session = await filterResult.first()

      expect(session).toBeTruthy()
      expect(session?._id).toBe(existingSession._id)
    })

    it("deve falhar quando usuario nao e agente", async () => {
      const clientUser = buildUser({ role: "CLIENT" })

      const role = clientUser.role?.toUpperCase() ?? ""
      const isAllowed = ["ADMIN", "MANAGER", "AGENT"].includes(role)

      expect(isAllowed).toBe(false)
    })
  })

  describe("endSession", () => {
    it("deve encerrar sessao ativa", async () => {
      const session = buildSession({ status: "ACTIVE" })
      const agent = buildUser({ role: "AGENT", tenantId: session.tenantId })

      expect(session.status).toBe("ACTIVE")

      const role = agent.role?.toUpperCase() ?? ""
      const canEnd = ["ADMIN", "MANAGER", "AGENT"].includes(role)

      expect(canEnd).toBe(true)
    })

    it("deve falhar quando sessao ja encerrada", async () => {
      const endedSession = buildSession({ status: "ENDED" })

      expect(endedSession.status).not.toBe("ACTIVE")
    })

    it("deve calcular duracao corretamente", async () => {
      const session = buildSession({
        startedAt: FIXED_NOW - 10_000,
      })

      const endedAt = FIXED_NOW
      const durationMs = endedAt - session.startedAt

      expect(durationMs).toBe(10_000)
    })
  })

  describe("postMachineMessage", () => {
    it("deve enviar mensagem quando token e sessao validos", async () => {
      const session = buildSession({ status: "ACTIVE" })
      const machine = buildMachine()
      const ticket = buildTicket({ machineId: machine._id })

      // Validar pre-condicoes
      expect(session.status).toBe("ACTIVE")
      expect(ticket.machineId?.toString()).toBe(machine._id.toString())
    })

    it("deve limitar tamanho da mensagem a 4000 caracteres", async () => {
      const longMessage = "a".repeat(4001)
      const isValid = longMessage.length <= 4000

      expect(isValid).toBe(false)

      const validMessage = "a".repeat(4000)
      expect(validMessage.length <= 4000).toBe(true)
    })

    it("deve falhar quando nao existe sessao ativa", async () => {
      const db = createMockDb()
      db.query.mockReturnValue({
        withIndex: vi.fn(() => ({
          filter: vi.fn(() => ({
            first: vi.fn(async () => null), // Nenhuma sessao
          })),
        })),
      })

      const queryResult = db.query("liveChatSessions")
      const session = await queryResult.withIndex().filter().first()

      expect(session).toBeNull()
    })
  })

  describe("autoEndInactiveSessions", () => {
    it("deve identificar sessoes inativas por mais de 5 minutos", async () => {
      const inactiveSession = buildSession({
        status: "ACTIVE",
        lastActivityAt: FIXED_NOW - FIVE_MINUTES_MS - 1000, // Inativa
      })

      const cutoffTime = FIXED_NOW - FIVE_MINUTES_MS
      const isInactive = inactiveSession.lastActivityAt < cutoffTime

      expect(isInactive).toBe(true)
    })

    it("nao deve encerrar sessoes ativas recentemente", async () => {
      const activeSession = buildSession({
        status: "ACTIVE",
        lastActivityAt: FIXED_NOW - 1000, // Ativa recentemente
      })

      const cutoffTime = FIXED_NOW - FIVE_MINUTES_MS
      const isInactive = activeSession.lastActivityAt < cutoffTime

      expect(isInactive).toBe(false)
    })

    it("deve respeitar limite maximo de sessoes por execucao", async () => {
      const maxSessionsPerRun = 50
      const sessions = Array.from({ length: 100 }, (_, i) =>
        buildSession({
          _id: `session_${i}` as Id<"liveChatSessions">,
          lastActivityAt: FIXED_NOW - FIVE_MINUTES_MS - 1000,
        })
      )

      const sessionsToProcess = sessions.slice(0, maxSessionsPerRun)

      expect(sessionsToProcess.length).toBe(50)
    })
  })

  describe("markMachineMessagesRead", () => {
    it("deve processar no maximo 50 mensagens por chamada", async () => {
      const maxMessages = 50
      const messageIds = Array.from(
        { length: 100 },
        (_, i) => `msg_${i}` as Id<"ticketChatMessages">
      )

      const messageIdsToProcess = messageIds.slice(0, maxMessages)

      expect(messageIdsToProcess.length).toBe(50)
    })

    it("deve ignorar mensagens ja lidas pelo usuario", async () => {
      const userId = "user_1" as Id<"users">
      const readBy = [{ userId, readAt: FIXED_NOW - 1000 }]

      const alreadyRead = readBy.some((r) => r.userId.toString() === userId.toString())

      expect(alreadyRead).toBe(true)
    })
  })

  describe("checkMachineUpdates (polling)", () => {
    it("deve retornar sessoes ativas para a maquina", async () => {
      const machine = buildMachine()
      const session = buildSession({ machineId: machine._id, status: "ACTIVE" })

      expect(session.machineId.toString()).toBe(machine._id.toString())
      expect(session.status).toBe("ACTIVE")
    })

    it("deve filtrar mensagens por lastCheckedAt", async () => {
      const lastCheckedAt = FIXED_NOW - 5000
      const messages = [
        { createdAt: FIXED_NOW - 10_000 }, // Antes do check
        { createdAt: FIXED_NOW - 3000 }, // Depois do check
        { createdAt: FIXED_NOW - 1000 }, // Depois do check
      ]

      const newMessages = messages.filter((m) => m.createdAt > lastCheckedAt)

      expect(newMessages.length).toBe(2)
    })
  })
})

describe("rate-limit", () => {
  it("deve respeitar limites de requisicoes", async () => {
    const limits = {
      CHAT_POLL: { maxRequests: 60, windowMs: 60_000 },
      CHAT_MESSAGES: { maxRequests: 30, windowMs: 60_000 },
      CHAT_SESSIONS: { maxRequests: 30, windowMs: 60_000 },
    }

    expect(limits.CHAT_POLL.maxRequests).toBe(60)
    expect(limits.CHAT_MESSAGES.maxRequests).toBe(30)
    expect(limits.CHAT_SESSIONS.maxRequests).toBe(30)
  })
})

describe("retry", () => {
  it("deve calcular backoff exponencial corretamente", async () => {
    const baseDelayMs = 100
    const maxDelayMs = 2000

    const delays = [0, 1, 2, 3].map((attempt) => {
      const exponentialDelay = baseDelayMs * Math.pow(2, attempt)
      return Math.min(exponentialDelay, maxDelayMs)
    })

    expect(delays[0]).toBe(100)
    expect(delays[1]).toBe(200)
    expect(delays[2]).toBe(400)
    expect(delays[3]).toBe(800)
  })

  it("deve respeitar maxDelayMs", async () => {
    const baseDelayMs = 100
    const maxDelayMs = 2000

    const attempt = 10 // 100 * 2^10 = 102400
    const exponentialDelay = baseDelayMs * Math.pow(2, attempt)
    const delay = Math.min(exponentialDelay, maxDelayMs)

    expect(delay).toBe(maxDelayMs)
  })
})
