import type { Logger } from '@linktr.ee/messaging-core'
import { render } from '@testing-library/react'
import React from 'react'
import { describe, it, expect, vi, afterEach } from 'vitest'

import {
  MessagingLoggerProvider,
  consoleLogger,
  resolveLogger,
  useMessagingLogger,
} from './index'

const ERROR_MESSAGE = '[MessagingShell] Failed to load channels:'

/** Logs one error through the seam the way a call site would. */
const ErrorEmitter: React.FC<{ error: Error }> = ({ error }) => {
  const logger = useMessagingLogger()
  logger.error(ERROR_MESSAGE, error)
  return null
}

/** Emits one line at every level so partial-sink fallback is observable. */
const AllLevelsEmitter: React.FC = () => {
  const logger = useMessagingLogger()
  logger.debug('debug line', 1)
  logger.info('info line', 2)
  logger.warn('warn line', 3)
  logger.error('error line', 4)
  return null
}

afterEach(() => {
  vi.restoreAllMocks()
})

describe('logger seam', () => {
  it('routes error to an injected sink with the exact message and raw error object', () => {
    const error = new Error('boom')
    const sink = { error: vi.fn() }

    render(
      <MessagingLoggerProvider logger={sink}>
        <ErrorEmitter error={error} />
      </MessagingLoggerProvider>
    )

    expect(sink.error).toHaveBeenCalledTimes(1)
    expect(sink.error).toHaveBeenCalledWith(ERROR_MESSAGE, error)
    // The raw Error instance, not a serialized copy.
    expect(sink.error.mock.calls[0][1]).toBe(error)
  })

  it('passes identical arguments to console.error when no sink is injected', () => {
    const error = new Error('boom')
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
    const sink = { error: vi.fn() }

    render(
      <MessagingLoggerProvider logger={sink}>
        <ErrorEmitter error={error} />
      </MessagingLoggerProvider>
    )
    const injectedArgs = sink.error.mock.calls[0]

    // No provider at all, and a provider with no logger prop: both must produce
    // byte-identical console output to what the injected sink received.
    render(<ErrorEmitter error={error} />)
    render(
      <MessagingLoggerProvider>
        <ErrorEmitter error={error} />
      </MessagingLoggerProvider>
    )

    expect(consoleError).toHaveBeenCalledTimes(2)
    expect(consoleError.mock.calls[0]).toEqual(injectedArgs)
    expect(consoleError.mock.calls[1]).toEqual(injectedArgs)
    expect(consoleError.mock.calls[0][1]).toBe(error)
  })

  it('falls back to console for the levels a partial sink omits', () => {
    const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
    const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
    const sink: Logger = { warn: vi.fn(), error: vi.fn() }

    render(
      <MessagingLoggerProvider logger={sink}>
        <AllLevelsEmitter />
      </MessagingLoggerProvider>
    )

    expect(sink.warn).toHaveBeenCalledWith('warn line', 3)
    expect(sink.error).toHaveBeenCalledWith('error line', 4)
    expect(consoleWarn).not.toHaveBeenCalled()
    expect(consoleError).not.toHaveBeenCalled()
    // debug and info are unset on the sink, so both land on console.log.
    expect(consoleLog.mock.calls).toEqual([
      ['debug line', 1],
      ['info line', 2],
    ])
  })

  it('defaults every level to console when the hook is used with no provider', () => {
    const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
    const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})

    render(<AllLevelsEmitter />)

    expect(consoleLog.mock.calls).toEqual([
      ['debug line', 1],
      ['info line', 2],
    ])
    expect(consoleWarn).toHaveBeenCalledWith('warn line', 3)
    expect(consoleError).toHaveBeenCalledWith('error line', 4)
  })

  it('resolveLogger keeps provided levels and fills the rest from console', () => {
    const warn = vi.fn()
    const resolved = resolveLogger({ warn })

    expect(resolved.warn).toBe(warn)
    expect(resolved.debug).toBe(consoleLogger.debug)
    expect(resolved.info).toBe(consoleLogger.info)
    expect(resolved.error).toBe(consoleLogger.error)
    expect(resolveLogger()).toEqual(consoleLogger)
  })

  it('consoleLogger dispatches through console at call time, not bind time', () => {
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})

    consoleLogger.error('late spy', 'arg')

    expect(consoleError).toHaveBeenCalledWith('late spy', 'arg')
  })
})
