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

const mocks = vi.hoisted(() => ({
  getProcessPage: vi.fn(),
  getProcessItem: vi.fn(),
  importProcess: vi.fn(),
  updateProcessing: vi.fn(),
  deleteProcessItem: vi.fn(),
  bulkUpdateProcess: vi.fn(),
  bulkDeleteProcess: vi.fn(),
}))

vi.mock('../api/client', async (importOriginal) => ({
  ...(await importOriginal<typeof import('../api/client')>()),
  getProcessPage: mocks.getProcessPage,
  getProcessItem: mocks.getProcessItem,
  importProcess: mocks.importProcess,
  updateProcessing: mocks.updateProcessing,
  deleteProcessItem: mocks.deleteProcessItem,
  bulkUpdateProcess: mocks.bulkUpdateProcess,
  bulkDeleteProcess: mocks.bulkDeleteProcess,
}))

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 item(overrides: Partial<ProcessItem> = {}): ProcessItem {
  return {
    id: 1,
    orgId: 1,
    seq: 1,
    siteName: '한국출판문화산업진흥원',
    category: '공지',
    boardName: '공지사항',
    postTitle: '2026년 사업 공고',
    url: 'https://example.com/1',
    description: null,
    hasAttachment: 'N',
    priorKoglType: '1유형',
    contractDocs: null,
    producedDate: '2026-01-01',
    publishedDate: '2026-01-02',
    reviewMajor: '만료 저작물',
    reviewMinor: null,
    reviewResult: '신유형 개방',
    reviewKoglType: '1유형',
    reviewAiType: null,
    reviewOpinion: null,
    reviewNote: null,
    priorEvidence: null,
    judgedKoglType: null,
    judgedAiType: null,
    finalOpinion: null,
    judgmentBasis: null,
    processStatus: null,
    webEditedAt: null,
    processedAt: null,
    createdAt: 0,
    updatedAt: 0,
    ...overrides,
  }
}

function page(overrides: Partial<ProcessPage> = {}): ProcessPage {
  return {
    items: [item()],
    total: 1,
    done: 0,
    page: 0,
    size: 30,
    ...overrides,
  }
}

beforeEach(() => {
  mocks.getProcessPage.mockReset()
  mocks.getProcessItem.mockReset()
  mocks.importProcess.mockReset()
  mocks.updateProcessing.mockReset()
  mocks.deleteProcessItem.mockReset()
  mocks.bulkUpdateProcess.mockReset()
  mocks.bulkDeleteProcess.mockReset()
})

describe('ProcessBoard 목록', () => {
  it('목록을 표로 보여주고 처리상태 배지가 나온다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => {
      expect(screen.getByText('2026년 사업 공고')).toBeTruthy()
      // "미처리"는 상태 필터 칩과 배지 둘 다에 나오므로 배지를 role로 구분해 확인한다.
      expect(screen.getAllByText('미처리').length).toBeGreaterThanOrEqual(2)
    })
    expect(screen.getByText('총 1건 · 처리완료 0건')).toBeTruthy()
  })

  it('자료가 없으면 안내 문구를 보여준다', async () => {
    mocks.getProcessPage.mockResolvedValue(page({ items: [], total: 0 }))

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => {
      expect(
        screen.getByText('업로드된 권리처리 자료가 없습니다. 엑셀 업로드로 시작하세요.'),
      ).toBeTruthy()
    })
  })

  it('상태 칩을 누르면 status 파라미터로 다시 조회한다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
        keyword: undefined,
        status: undefined,
        page: 0,
        size: 30,
      }),
    )

    fireEvent.click(screen.getByRole('button', { name: '처리완료' }))

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
        keyword: undefined,
        status: 'DONE',
        page: 0,
        size: 30,
      }),
    )

    fireEvent.click(screen.getByRole('button', { name: '미처리' }))

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
        keyword: undefined,
        status: 'PENDING',
        page: 0,
        size: 30,
      }),
    )
  })

  it('검색어를 입력하고 검색을 누르면 keyword로 다시 조회한다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(mocks.getProcessPage).toHaveBeenCalledTimes(1))

    fireEvent.change(screen.getByLabelText('검색어'), { target: { value: '출판' } })
    fireEvent.click(screen.getByRole('button', { name: '검색' }))

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
        keyword: '출판',
        status: undefined,
        page: 0,
        size: 30,
      }),
    )
  })

  it('엑셀 업로드를 하면 importProcess를 호출하고 결과를 보여준다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.importProcess.mockResolvedValue({ created: 3, updated: 2, total: 5 })

    const { container } = render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(mocks.getProcessPage).toHaveBeenCalledTimes(1))

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

    await waitFor(() => expect(mocks.importProcess).toHaveBeenCalledWith(1, file))
    await waitFor(() => {
      expect(screen.getByText('신규 3 · 갱신 2 · 전체 5')).toBeTruthy()
    })
  })

  it('처리완료 건은 수정 버튼이, 미처리 건은 처리등록 버튼만 보인다', async () => {
    mocks.getProcessPage.mockResolvedValue(page({ items: [item({ processStatus: '처리완료' })] }))

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    expect(screen.getByRole('button', { name: '수정' })).toBeTruthy()
    expect(screen.queryByRole('button', { name: '처리등록' })).toBeNull()
    // 개별 행 삭제는 없앴다(상단 [선택 삭제] 또는 상세 화면에서 지운다)
    expect(screen.queryByRole('button', { name: '삭제' })).toBeNull()
  })

  it('행을 체크하고 [선택 삭제]를 누르면 확인 후 일괄 삭제한다', async () => {
    mocks.getProcessPage.mockResolvedValue(page({ items: [item({ processStatus: '처리완료' })] }))
    mocks.bulkDeleteProcess.mockResolvedValue({ deleted: 1 })
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())

    fireEvent.click(screen.getByLabelText('1번 선택'))
    fireEvent.click(screen.getByRole('button', { name: '선택 삭제' }))

    expect(confirmSpy).toHaveBeenCalled()
    await waitFor(() => expect(mocks.bulkDeleteProcess).toHaveBeenCalledWith(1, [1]))

    confirmSpy.mockRestore()
  })

  it('체크한 건의 처리상태와 최종의견을 일괄 반영한다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.bulkUpdateProcess.mockResolvedValue({ updated: 1 })

    render(withCodes(<ProcessBoard org={org()} />))
    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())

    fireEvent.click(screen.getByLabelText('1번 선택'))
    fireEvent.change(screen.getByLabelText('일괄 처리상태'), { target: { value: '처리완료' } })
    fireEvent.change(screen.getByLabelText('일괄 최종의견'), { target: { value: '일괄 처리' } })
    fireEvent.click(screen.getByRole('button', { name: '일괄 등록' }))

    await waitFor(() =>
      expect(mocks.bulkUpdateProcess).toHaveBeenCalledWith(1, {
        ids: [1],
        processStatus: '처리완료',
        finalOpinion: '일괄 처리',
        judgedKoglType: undefined,
      }),
    )
    expect(await screen.findByText('1건에 반영했습니다.')).toBeTruthy()
  })

  it('상세검색 3칸은 열별 조건으로 함께 넘어간다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())

    render(withCodes(<ProcessBoard org={org()} />))
    await waitFor(() => expect(mocks.getProcessPage).toHaveBeenCalled())

    fireEvent.click(screen.getByRole('button', { name: '상세검색' }))
    fireEvent.change(screen.getByLabelText('사이트명'), { target: { value: '누리집' } })
    fireEvent.change(screen.getByLabelText('게시판명'), { target: { value: '공고' } })
    fireEvent.change(screen.getByLabelText('게시물 제목'), { target: { value: '사업' } })
    fireEvent.click(screen.getByRole('button', { name: '검색' }))

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenLastCalledWith(1, {
        keyword: undefined,
        site: '누리집',
        board: '공고',
        title: '사업',
        status: undefined,
        page: 0,
        size: 30,
      }),
    )
  })

  it('다음 페이지 버튼을 누르면 page+1로 다시 조회한다', async () => {
    mocks.getProcessPage.mockResolvedValue(page({ total: 31 }))

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
        keyword: undefined,
        status: undefined,
        page: 0,
        size: 30,
      }),
    )

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

    await waitFor(() =>
      expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
        keyword: undefined,
        status: undefined,
        page: 1,
        size: 30,
      }),
    )
  })
})

describe('ProcessBoard 상세', () => {
  it('처리등록을 누르면 상세 화면이 열리고 조회한 값이 채워진다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.getProcessItem.mockResolvedValue(item({ finalOpinion: '검토 완료' }))

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))

    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalledWith(1, 1))
    await waitFor(() => {
      expect((screen.getByLabelText('최종의견') as HTMLTextAreaElement).value).toBe('검토 완료')
    })
  })

  it('권리처리상태를 선택하지 않으면 저장 버튼이 비활성이고, 고르면 활성화된다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.getProcessItem.mockResolvedValue(item())

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled())

    expect(screen.getByRole('button', { name: '수정' })).toBeDisabled()

    fireEvent.change(screen.getByLabelText('*권리처리상태'), { target: { value: '처리완료' } })

    expect(screen.getByRole('button', { name: '수정' })).not.toBeDisabled()
  })

  it('계약서 유무 체크박스 조합과 기타 입력이 쉼표로 연결되어 저장 페이로드에 실린다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.getProcessItem.mockResolvedValue(item())
    mocks.updateProcessing.mockResolvedValue(item({ processStatus: '처리완료' }))

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled())

    fireEvent.click(screen.getByRole('checkbox', { name: '양도계약서' }))
    fireEvent.click(screen.getByRole('checkbox', { name: '공문' }))
    fireEvent.click(screen.getByRole('checkbox', { name: '기타' }))
    fireEvent.change(screen.getByLabelText('기타 계약서 내용'), { target: { value: '직접 촬영본 사용동의' } })
    fireEvent.change(screen.getByLabelText('*권리처리상태'), { target: { value: '처리완료' } })
    fireEvent.click(screen.getByRole('button', { name: '수정' }))

    await waitFor(() =>
      expect(mocks.updateProcessing).toHaveBeenCalledWith(1, 1, {
        contractDocs: '양도계약서, 공문, 기타:직접 촬영본 사용동의',
        judgedKoglType: null,
        judgedAiType: null,
        finalOpinion: null,
        judgmentBasis: null,
        processStatus: '처리완료',
      }),
    )

    // 저장 후 목록으로 복귀
    await waitFor(() => expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy())
  })

  it('기존에 저장된 계약서 유무 값을 체크박스 상태로 역파싱해 보여준다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.getProcessItem.mockResolvedValue(
      item({ contractDocs: '양도계약서, 기타:구두 동의' }),
    )

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled())

    expect(screen.getByRole('checkbox', { name: '양도계약서' })).toBeChecked()
    expect(screen.getByRole('checkbox', { name: '기타' })).toBeChecked()
    expect((screen.getByLabelText('기타 계약서 내용') as HTMLInputElement).value).toBe('구두 동의')
  })

  it('목록 버튼을 누르면 저장 없이 목록으로 돌아간다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.getProcessItem.mockResolvedValue(item())

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled())

    fireEvent.click(screen.getByRole('button', { name: '목록' }))

    expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy()
    expect(mocks.updateProcessing).not.toHaveBeenCalled()
  })

  it('상세 화면에서 삭제를 누르면 확인 후 deleteProcessItem을 호출하고 목록으로 돌아간다', async () => {
    mocks.getProcessPage.mockResolvedValue(page())
    mocks.getProcessItem.mockResolvedValue(item())
    mocks.deleteProcessItem.mockResolvedValue(undefined)
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)

    render(withCodes(<ProcessBoard org={org()} />))

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled())

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

    expect(confirmSpy).toHaveBeenCalled()
    await waitFor(() => expect(mocks.deleteProcessItem).toHaveBeenCalledWith(1, 1))
    await waitFor(() => expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy())

    confirmSpy.mockRestore()
  })
})
