import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ChannelPosts from './ChannelPosts'
import type { Org, PostView } from '../api/client'

const mocks = vi.hoisted(() => ({
  getPosts: vi.fn(),
  getNotice: vi.fn(),
  sendMessage: vi.fn(),
  clearNotice: vi.fn(),
}))

vi.mock('../api/client', async (importOriginal) => ({
  ...(await importOriginal<typeof import('../api/client')>()),
  getPosts: mocks.getPosts,
  getNotice: mocks.getNotice,
  sendMessage: mocks.sendMessage,
  clearNotice: mocks.clearNotice,
}))

function org(overrides: Partial<Org> = {}): Org {
  return {
    id: 1,
    orgNo: '001',
    orgName: '국제방송교류재단',
    status: 'ACTIVE',
    applicant: null,
    mj: null,
    itn: null,
    lawyer: null,
    lawyerAssignedDate: null,
    channelIdMj: 'chan-mj',
    channelIdLaw: 'chan-law',
    stage: null,
    ...overrides,
  }
}

function post(overrides: Partial<PostView> = {}): PostView {
  return {
    id: 'p1',
    user: '송민지',
    message: '안녕하세요',
    createAt: Date.now(),
    system: false,
    files: [],
    ...overrides,
  }
}

beforeEach(() => {
  mocks.getPosts.mockReset()
  mocks.getNotice.mockReset().mockResolvedValue(null)
  mocks.sendMessage.mockReset().mockResolvedValue({ post: post(), noticeWarning: null })
  mocks.clearNotice.mockReset().mockResolvedValue(undefined)
  vi.useRealTimers()
})

describe('ChannelPosts', () => {
  it('메시지를 작성자와 본문과 함께 보여준다', async () => {
    mocks.getPosts.mockResolvedValue([post()])

    render(<ChannelPosts org={org()} />)

    await waitFor(() => {
      expect(screen.getByText('송민지')).toBeTruthy()
      expect(screen.getByText('안녕하세요')).toBeTruthy()
    })
  })

  it('시스템 메시지는 회색 중앙 정렬 한 줄로 보여준다', async () => {
    mocks.getPosts.mockResolvedValue([post({ system: true, message: '님이 입장했습니다.' })])

    render(<ChannelPosts org={org()} />)

    await waitFor(() => {
      const line = screen.getByText('님이 입장했습니다.')
      expect(line.className).toContain('text-gray-400')
      expect(line.className).toContain('text-center')
    })
  })

  it('3초마다 폴링해서 게시글을 다시 조회한다', async () => {
    vi.useFakeTimers()
    mocks.getPosts.mockResolvedValue([])

    render(<ChannelPosts org={org()} />)

    await act(async () => {
      await vi.advanceTimersByTimeAsync(0)
    })
    expect(mocks.getPosts).toHaveBeenCalledTimes(1)

    await act(async () => {
      await vi.advanceTimersByTimeAsync(3000)
    })
    expect(mocks.getPosts).toHaveBeenCalledTimes(2)

    vi.useRealTimers()
  })

  it('양쪽 채널이 모두 없으면 안내 문구를 보여주고 API를 호출하지 않는다', () => {
    render(<ChannelPosts org={org({ channelIdMj: null, channelIdLaw: null })} />)

    expect(screen.getByText('채널이 아직 생성되지 않았습니다.')).toBeTruthy()
    expect(mocks.getPosts).not.toHaveBeenCalled()
  })

  it('법률검토 채널이 없으면 법률검토 칩이 비활성화된다', async () => {
    mocks.getPosts.mockResolvedValue([])
    render(<ChannelPosts org={org({ channelIdLaw: null })} />)

    expect(screen.getByRole('button', { name: '법률검토' })).toBeDisabled()
    await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
  })

  it('같은 사람이 5분 안에 연달아 쓴 메시지는 이름과 아바타를 한 번만 보여준다', async () => {
    const now = Date.now()
    mocks.getPosts.mockResolvedValue([
      post({ id: 'p1', createAt: now }),
      post({ id: 'p2', message: '이어서 씁니다', createAt: now + 1000 }),
    ])

    render(<ChannelPosts org={org()} />)

    await waitFor(() => expect(screen.getByText('이어서 씁니다')).toBeTruthy())
    expect(screen.getAllByText('송민지')).toHaveLength(1)
  })

  it('첨부파일 칩의 링크는 다운로드 엔드포인트를 가리킨다', async () => {
    mocks.getPosts.mockResolvedValue([
      post({ files: [{ id: 'f1', name: '보고서.pdf', size: 1200 }] }),
    ])

    render(<ChannelPosts org={org()} />)

    await waitFor(() => {
      const link = screen.getByText('보고서.pdf').closest('a')
      expect(link).toHaveAttribute('href', '/api/files/f1')
    })
  })

  it('보내기는 내용이 없으면 비활성이고, 전송하면 입력창을 비운다', async () => {
    mocks.getPosts.mockResolvedValue([])

    render(<ChannelPosts org={org()} />)

    const sendButton = screen.getByRole('button', { name: '보내기' })
    expect(sendButton).toBeDisabled()

    fireEvent.change(screen.getByLabelText('메시지 입력'), { target: { value: '안녕하세요' } })
    expect(sendButton).toBeEnabled()

    fireEvent.click(sendButton)

    await waitFor(() =>
      expect(mocks.sendMessage).toHaveBeenCalledWith(1, 'mj', {
        message: '안녕하세요',
        notice: false,
        files: [],
      }),
    )
    await waitFor(() => expect(screen.getByLabelText('메시지 입력')).toHaveValue(''))
  })

  it('Enter는 전송하고 Shift_Enter는 전송하지 않는다', async () => {
    mocks.getPosts.mockResolvedValue([])

    render(<ChannelPosts org={org()} />)

    const textarea = screen.getByLabelText('메시지 입력')
    fireEvent.change(textarea, { target: { value: '안녕' } })

    fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true })
    expect(mocks.sendMessage).not.toHaveBeenCalled()

    fireEvent.keyDown(textarea, { key: 'Enter' })
    await waitFor(() => expect(mocks.sendMessage).toHaveBeenCalledTimes(1))
  })

  it('공지로 등록을 체크하면 안내가 보이고 notice_true로 전송된다', async () => {
    mocks.getPosts.mockResolvedValue([])

    render(<ChannelPosts org={org()} />)

    fireEvent.click(screen.getByLabelText('공지로 등록'))
    expect(screen.getByText(/이전 공지는 해제됩니다/)).toBeTruthy()

    fireEvent.change(screen.getByLabelText('메시지 입력'), { target: { value: '중요 안내' } })
    fireEvent.click(screen.getByRole('button', { name: '보내기' }))

    await waitFor(() =>
      expect(mocks.sendMessage).toHaveBeenCalledWith(1, 'mj', {
        message: '중요 안내',
        notice: true,
        files: [],
      }),
    )
  })

  it('파일을 선택하면 칩이 보이고 제거할 수 있다', async () => {
    mocks.getPosts.mockResolvedValue([])

    render(<ChannelPosts org={org()} />)

    const input = screen.getByLabelText('첨부파일 선택')
    const file = new File(['x'], '계약서.pdf', { type: 'application/pdf' })
    fireEvent.change(input, { target: { files: [file] } })

    expect(screen.getByText('계약서.pdf')).toBeTruthy()

    fireEvent.click(screen.getByRole('button', { name: '계약서.pdf 제거' }))
    expect(screen.queryByText('계약서.pdf')).toBeNull()
  })

  it('입력창에 파일을 드래그앤드롭하면 첨부된다', async () => {
    mocks.getPosts.mockResolvedValue([])

    render(<ChannelPosts org={org()} />)

    const dropzone = screen.getByTestId('composer-dropzone')
    const file = new File(['x'], '드랍파일.pdf', { type: 'application/pdf' })

    fireEvent.dragEnter(dropzone, { dataTransfer: { files: [file] } })
    expect(screen.getByText('파일을 여기에 놓으면 첨부됩니다')).toBeTruthy()

    fireEvent.drop(dropzone, { dataTransfer: { files: [file] } })

    expect(screen.getByText('드랍파일.pdf')).toBeTruthy()
    expect(screen.queryByText('파일을 여기에 놓으면 첨부됩니다')).toBeNull()
    expect(screen.getByRole('button', { name: '보내기' })).toBeEnabled()
  })

  it('공지가 있으면 배너로 보이고 확인 후 해제할 수 있다', async () => {
    mocks.getPosts.mockResolvedValue([])
    mocks.getNotice.mockResolvedValue(post({ id: 'n1', message: '중요 공지입니다' }))
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)

    render(<ChannelPosts org={org()} />)

    await waitFor(() => expect(screen.getByText('📢 공지')).toBeTruthy())
    expect(screen.getByText('중요 공지입니다')).toBeTruthy()

    fireEvent.click(screen.getByRole('button', { name: '공지 해제' }))

    await waitFor(() => expect(mocks.clearNotice).toHaveBeenCalledWith(1, 'mj'))
    confirmSpy.mockRestore()
  })

  it('공지 뒤처리 경고가 오면 표시한다', async () => {
    mocks.getPosts.mockResolvedValue([])
    mocks.sendMessage.mockResolvedValue({
      post: post(),
      noticeWarning: '공지 등록 처리 중 일부가 실패했습니다: 헤더 갱신 실패',
    })

    render(<ChannelPosts org={org()} />)

    fireEvent.change(screen.getByLabelText('메시지 입력'), { target: { value: '공지' } })
    fireEvent.click(screen.getByRole('button', { name: '보내기' }))

    await waitFor(() => expect(screen.getByText(/일부가 실패했습니다/)).toBeTruthy())
  })
})
