import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ContactsPage from './ContactsPage'
import { withCodes } from '../codes/fixtures'
import type { Contact } from '../api/client'

const mocks = vi.hoisted(() => ({
  getContacts: vi.fn(),
  createContact: vi.fn(),
  updateContactInfo: vi.fn(),
  deleteContact: vi.fn(),
  importMembers: vi.fn(),
}))

vi.mock('../api/client', async (importOriginal) => ({
  ...(await importOriginal<typeof import('../api/client')>()),
  getContacts: mocks.getContacts,
  createContact: mocks.createContact,
  updateContactInfo: mocks.updateContactInfo,
  deleteContact: mocks.deleteContact,
  importMembers: mocks.importMembers,
}))

function contact(overrides: Partial<Contact> = {}): Contact {
  return {
    id: 1,
    category: 'APPLICANT',
    name: '송민지',
    affiliation: '국제방송교류재단',
    deptName: '데이터정보화팀',
    title: '과장',
    phone: '02-3475-5434',
    email: 'ming@arirang.com',
    ...overrides,
  }
}

beforeEach(() => {
  mocks.getContacts.mockReset()
  mocks.createContact.mockReset()
  mocks.updateContactInfo.mockReset()
  mocks.deleteContact.mockReset()
  mocks.importMembers.mockReset()
})

describe('ContactsPage', () => {
  it('담당자 목록을 구분·성명 등과 함께 보여준다', async () => {
    mocks.getContacts.mockResolvedValue([contact()])

    render(withCodes(<ContactsPage />))

    await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())
    expect(screen.getByRole('cell', { name: '신청기관' })).toBeTruthy()
    expect(screen.getByText('국제방송교류재단')).toBeTruthy()
    expect(screen.getByText('데이터정보화팀')).toBeTruthy()
    expect(screen.getByText('02-3475-5434')).toBeTruthy()
    expect(screen.getByText('ming@arirang.com')).toBeTruthy()
  })

  it('담당자가 없으면 안내 문구를 보여준다', async () => {
    mocks.getContacts.mockResolvedValue([])

    render(withCodes(<ContactsPage />))

    await waitFor(() => {
      expect(screen.getByText('등록된 담당자가 없습니다.')).toBeTruthy()
    })
  })

  it('필터 칩을 누르면 해당 구분만 보인다', async () => {
    mocks.getContacts.mockResolvedValue([
      contact({ id: 1, name: '송민지', category: 'APPLICANT' }),
      contact({ id: 2, name: '이변호', category: 'LAWYER', affiliation: null, deptName: null }),
    ])

    render(withCodes(<ContactsPage />))

    await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())

    fireEvent.click(screen.getByRole('button', { name: '변호사' }))

    expect(screen.queryByText('송민지')).toBeNull()
    expect(screen.getByText('이변호')).toBeTruthy()
  })

  it('수행기관 필터 칩을 누르면 수행기관만 보인다', async () => {
    mocks.getContacts.mockResolvedValue([
      contact({ id: 1, name: '송민지', category: 'APPLICANT' }),
      contact({ id: 2, name: '홍수행', category: 'OPERATOR', affiliation: '수행사', deptName: '운영팀' }),
    ])

    render(withCodes(<ContactsPage />))

    await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())

    fireEvent.click(screen.getByRole('button', { name: '수행기관' }))

    expect(screen.queryByText('송민지')).toBeNull()
    expect(screen.getByText('홍수행')).toBeTruthy()
  })

  it('담당자 추가를 누르면 모달이 뜨고, 구분과 성명을 채워야 저장이 활성화된다', async () => {
    mocks.getContacts.mockResolvedValue([])

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(mocks.getContacts).toHaveBeenCalled())

    fireEvent.click(screen.getByRole('button', { name: '담당자 추가' }))

    expect(screen.getByRole('heading', { name: '담당자 추가' })).toBeTruthy()
    const save = screen.getByRole('button', { name: '저장' })
    expect(save).toBeDisabled()

    fireEvent.change(screen.getByLabelText('성명'), { target: { value: '홍길동' } })
    expect(save).toBeEnabled()
  })

  it('저장하면 createContact가 폼 값으로 호출되고, 이후 목록을 다시 불러온다', async () => {
    mocks.getContacts.mockResolvedValueOnce([]).mockResolvedValueOnce([contact({ name: '홍길동' })])
    mocks.createContact.mockResolvedValue(contact({ name: '홍길동' }))

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(mocks.getContacts).toHaveBeenCalledTimes(1))

    fireEvent.click(screen.getByRole('button', { name: '담당자 추가' }))
    fireEvent.change(screen.getByLabelText('성명'), { target: { value: '홍길동' } })
    fireEvent.change(screen.getByLabelText('구분'), { target: { value: 'MJ' } })
    fireEvent.click(screen.getByRole('button', { name: '저장' }))

    await waitFor(() => {
      expect(mocks.createContact).toHaveBeenCalledWith({
        category: 'MJ',
        name: '홍길동',
        affiliation: '',
        deptName: '',
        title: '',
        phone: '',
        email: '',
      })
    })

    await waitFor(() => expect(mocks.getContacts).toHaveBeenCalledTimes(2))
  })

  it('수정 버튼을 누르면 기존 값이 채워진 모달이 뜨고, 저장하면 updateContactInfo가 호출된다', async () => {
    mocks.getContacts.mockResolvedValue([contact()])
    mocks.updateContactInfo.mockResolvedValue(contact({ phone: '02-0000-0000' }))

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())

    fireEvent.click(screen.getByRole('button', { name: '수정' }))

    expect(screen.getByText('담당자 수정')).toBeTruthy()
    expect(screen.getByLabelText('성명')).toHaveValue('송민지')

    fireEvent.change(screen.getByLabelText('연락처'), { target: { value: '02-0000-0000' } })
    fireEvent.click(screen.getByRole('button', { name: '저장' }))

    await waitFor(() => {
      expect(mocks.updateContactInfo).toHaveBeenCalledWith(
        1,
        expect.objectContaining({ phone: '02-0000-0000' }),
      )
    })
  })

  it('삭제를 누르면 확인을 물어보고, 확인하면 deleteContact를 호출한다', async () => {
    mocks.getContacts.mockResolvedValue([contact()])
    mocks.deleteContact.mockResolvedValue(undefined)
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())

    fireEvent.click(screen.getByRole('button', { name: '삭제' }))

    expect(confirmSpy).toHaveBeenCalledWith(
      '삭제하면 기관에 지정된 배정도 함께 해제됩니다. 삭제할까요?',
    )
    await waitFor(() => expect(mocks.deleteContact).toHaveBeenCalledWith(1))

    confirmSpy.mockRestore()
  })

  it('삭제 확인을 취소하면 deleteContact를 호출하지 않는다', async () => {
    mocks.getContacts.mockResolvedValue([contact()])
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false)

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())

    fireEvent.click(screen.getByRole('button', { name: '삭제' }))

    expect(confirmSpy).toHaveBeenCalled()
    expect(mocks.deleteContact).not.toHaveBeenCalled()

    confirmSpy.mockRestore()
  })

  it('회원명단 업로드 버튼으로 파일을 고르면 importMembers가 호출되고 결과가 표시된다', async () => {
    mocks.getContacts.mockResolvedValue([])
    mocks.importMembers.mockResolvedValue({ created: 2, updated: 1, skipped: 1, assigned: 1 })

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(mocks.getContacts).toHaveBeenCalled())

    const file = new File(['dummy'], 'members.xlsx', {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    })
    const input = document.querySelector('input[type="file"]') as HTMLInputElement
    fireEvent.change(input, { target: { files: [file] } })

    await waitFor(() => expect(mocks.importMembers).toHaveBeenCalledWith(file))
    expect(
      screen.getByText('등록 2 · 갱신 1 · 기관지정 1 · 건너뜀 1'),
    ).toBeTruthy()
  })

  it('회원명단 업로드가 실패하면 오류 메시지를 보여준다', async () => {
    mocks.getContacts.mockResolvedValue([])
    mocks.importMembers.mockRejectedValue(new Error('파일 형식이 올바르지 않습니다.'))

    render(withCodes(<ContactsPage />))
    await waitFor(() => expect(mocks.getContacts).toHaveBeenCalled())

    const file = new File(['dummy'], 'members.xlsx', {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    })
    const input = document.querySelector('input[type="file"]') as HTMLInputElement
    fireEvent.change(input, { target: { files: [file] } })

    await waitFor(() => {
      expect(screen.getByText('파일 형식이 올바르지 않습니다.')).toBeTruthy()
    })
  })
})
