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

const mocks = vi.hoisted(() => ({
  getReviewPage: vi.fn(),
  getReviewItem: vi.fn(),
  importReview: vi.fn(),
  updateReviewJudgment: vi.fn(),
  deleteReviewItem: vi.fn(),
  bulkUpdateReview: vi.fn(),
  bulkDeleteReview: vi.fn(),
}))

vi.mock('../api/client', async (importOriginal) => ({
  ...(await importOriginal<typeof import('../api/client')>()),
  getReviewPage: mocks.getReviewPage,
  getReviewItem: mocks.getReviewItem,
  importReview: mocks.importReview,
  updateReviewJudgment: mocks.updateReviewJudgment,
  deleteReviewItem: mocks.deleteReviewItem,
  bulkUpdateReview: mocks.bulkUpdateReview,
  bulkDeleteReview: mocks.bulkDeleteReview,
}))

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<ReviewItem> = {}): ReviewItem {
  return {
    id: 1,
    orgId: 1,
    seq: 1,
    siteName: '한국출판문화산업진흥원',
    category: '공지',
    boardPath: '게시판>공지',
    boardName: '공지사항',
    postTitle: '2026년 사업 공고',
    url: 'https://example.com/1',
    postRegistered: '2026-01-01',
    producedDate: '20260101',
    publishedDate: '20260102',
    hasAttachment: 'N',
    koglAttached: 'Y',
    koglType: '1유형',
    aiType: null,
    surveyorNote: null,
    openable: null,
    reviewMajor: null,
    reviewMinor: null,
    reviewResult: '신유형 개방',
    judgedKoglType: null,
    judgedAiType: null,
    opinion: null,
    lawyerNote: null,
    needsProcessing: null,
    webEditedAt: null,
    createdAt: 0,
    updatedAt: 0,
    ...overrides,
  }
}

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

beforeEach(() => {
  mocks.getReviewPage.mockReset()
  mocks.getReviewItem.mockReset()
  mocks.importReview.mockReset()
  mocks.updateReviewJudgment.mockReset()
  mocks.deleteReviewItem.mockReset()
  mocks.bulkUpdateReview.mockReset()
  mocks.bulkDeleteReview.mockReset()
})

describe('ReviewBoard 목록', () => {
  it('목록을 표로 보여주고 처리결과 배지가 나온다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())

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

    await waitFor(() => {
      expect(screen.getByText('2026년 사업 공고')).toBeTruthy()
    })
    // '신유형 개방'은 일괄 등록 셀렉트의 옵션에도 있으므로 표 안에서만 찾는다
    const table = within(screen.getByRole('table'))
    expect(table.getByText('신유형 개방')).toBeTruthy()
    expect(screen.getByText('총 1건 · 처리 0건')).toBeTruthy()
  })

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // 개별 행에는 삭제 버튼이 없다(상단 일괄 삭제로 옮겼다)
    expect(screen.getByRole('button', { name: '선택 삭제' })).toBeDisabled()

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

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

    confirmSpy.mockRestore()
  })

  it('전체 선택 체크박스로 페이지의 모든 행을 고른다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())

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

    fireEvent.click(screen.getByLabelText('전체 선택'))
    expect(screen.getByText('선택 1건')).toBeTruthy()
    expect(screen.getByLabelText('1번 선택')).toBeChecked()

    fireEvent.click(screen.getByLabelText('전체 선택'))
    expect(screen.getByText('선택 0건')).toBeTruthy()
  })

  it('체크한 건에 유형과 의견을 일괄 반영한다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())
    mocks.bulkUpdateReview.mockResolvedValue({ updated: 1 })

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

    fireEvent.click(screen.getByLabelText('1번 선택'))
    fireEvent.change(screen.getByLabelText('일괄 공공누리 유형'), { target: { value: '0유형' } })
    fireEvent.change(screen.getByLabelText('일괄 의견'), { target: { value: '일괄 검토 완료' } })
    fireEvent.click(screen.getByRole('button', { name: '일괄 등록' }))

    await waitFor(() =>
      expect(mocks.bulkUpdateReview).toHaveBeenCalledWith(1, {
        ids: [1],
        judgedKoglType: '0유형',
        opinion: '일괄 검토 완료',
        reviewResult: undefined,
      }),
    )
    expect(await screen.findByText('1건에 반영했습니다.')).toBeTruthy()
  })

  it('아무 값도 채우지 않고 일괄 등록하면 안내만 하고 호출하지 않는다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())

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

    fireEvent.click(screen.getByLabelText('1번 선택'))
    fireEvent.click(screen.getByRole('button', { name: '일괄 등록' }))

    expect(await screen.findByText('반영할 값을 하나 이상 채워주세요.')).toBeTruthy()
    expect(mocks.bulkUpdateReview).not.toHaveBeenCalled()
  })

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

    render(withCodes(<ReviewBoard org={org()} />))
    await waitFor(() => expect(mocks.getReviewPage).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.getReviewPage).toHaveBeenLastCalledWith(1, {
        keyword: undefined,
        site: '누리집',
        board: '공고',
        title: '사업',
        page: 0,
        size: 30,
      }),
    )
  })

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

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

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

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

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

describe('ReviewBoard 상세', () => {
  it('수정을 누르면 상세 화면이 열리고 조회한 값이 채워진다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())
    mocks.getReviewItem.mockResolvedValue(
      item({ reviewMajor: '만료 저작물', opinion: '검토 완료' }),
    )

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

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

    await waitFor(() => expect(mocks.getReviewItem).toHaveBeenCalledWith(1, 1))
    await waitFor(() => {
      expect((screen.getByLabelText('의견') as HTMLTextAreaElement).value).toBe('검토 완료')
    })
    expect(screen.getByRole('radio', { name: '만료 저작물' })).toBeChecked()
  })

  it('대분류를 바꾸면 처리결과가 자동으로 채워지지만, 수동으로 고르면 이후 대분류 변경에도 유지된다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())
    mocks.getReviewItem.mockResolvedValue(item({ reviewMajor: null, reviewResult: null }))

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

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '수정' }))
    await waitFor(() => expect(mocks.getReviewItem).toHaveBeenCalled())

    fireEvent.click(screen.getByRole('radio', { name: '만료 저작물' }))
    expect(screen.getByRole('radio', { name: '신유형 개방' })).toBeChecked()

    // 수동으로 다른 처리결과를 고른다
    fireEvent.click(screen.getByRole('radio', { name: '권리처리 추진' }))
    expect(screen.getByRole('radio', { name: '권리처리 추진' })).toBeChecked()

    // 대분류를 다시 바꿔도(원래대로면 "신유형 개방"을 추천) 수동 선택이 유지된다
    fireEvent.click(screen.getByRole('radio', { name: '업무상 저작물' }))
    expect(screen.getByRole('radio', { name: '권리처리 추진' })).toBeChecked()
  })

  describe('제3자 권리처리 정책(2026-07-20 변경)', () => {
    /** 상세 편집 화면을 열고 제3자 권리 + 지정한 세부항목까지 고른다. */
    async function openDetailWithMinor(minor: string, itemOverrides = {}) {
      mocks.getReviewPage.mockResolvedValue(page())
      mocks.getReviewItem.mockResolvedValue(
        item({ reviewMajor: null, reviewMinor: null, reviewResult: null, judgedKoglType: null, ...itemOverrides }),
      )
      render(withCodes(<ReviewBoard org={org()} />))
      await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
      fireEvent.click(screen.getByRole('button', { name: '수정' }))
      await waitFor(() => expect(mocks.getReviewItem).toHaveBeenCalled())

      fireEvent.click(screen.getByRole('radio', { name: '제3자 권리' }))
      fireEvent.click(screen.getByRole('radio', { name: minor }))
    }

    it('권리 일부 보유를 고르면 기본값이 권리처리 추진이고 공공누리 유형은 보류가 된다', async () => {
      await openDetailWithMinor('3. 권리 일부(공동) 보유')

      expect(screen.getByRole('radio', { name: '권리처리 추진' })).toBeChecked()
      expect(screen.getByRole('radio', { name: '공공누리 유형 보류' })).toBeChecked()
    })

    it('미희망을 고르면 기존 공공누리 유형을 그대로 쓰고 비고에 사유를 남긴다', async () => {
      await openDetailWithMinor('4. 권리 미보유', { koglType: '2유형' })

      fireEvent.click(screen.getByRole('radio', { name: '권리처리 추진 미희망' }))

      expect(screen.getByRole('radio', { name: '공공누리 유형 2유형' })).toBeChecked()
      expect((screen.getByLabelText('비고') as HTMLTextAreaElement).value).toContain(
        '권리처리 추진 미희망에 따른 기존 공공누리 부착',
      )
    })

    it('미희망인데 기존 공공누리 유형이 없으면 유형을 비워 둔다', async () => {
      await openDetailWithMinor('4. 권리 미보유', { koglType: null })

      fireEvent.click(screen.getByRole('radio', { name: '권리처리 추진 미희망' }))

      for (const option of ['0유형', '1유형', '2유형', '3유형', '4유형', '보류', '개방불가']) {
        expect(screen.getByRole('radio', { name: `공공누리 유형 ${option}` })).not.toBeChecked()
      }
      expect((screen.getByLabelText('비고') as HTMLTextAreaElement).value).toBe('')
    })

    it('3·4번에서는 신유형 개방·개방불가를 고를 수 없다', async () => {
      await openDetailWithMinor('3. 권리 일부(공동) 보유')

      expect(screen.getByRole('radio', { name: '신유형 개방' })).toBeDisabled()
      expect(screen.getByRole('radio', { name: '계약서 등 재확인' })).toBeDisabled()
      expect(screen.getByRole('radio', { name: '개방불가' })).toBeDisabled()
      expect(screen.getByRole('radio', { name: '권리처리 추진 미희망' })).toBeEnabled()
      // 공공누리 유형의 개방불가는 처리결과와 별개로 남아 있다
      expect(screen.getByRole('radio', { name: '공공누리 유형 개방불가' })).toBeEnabled()
    })

    it('초상권 포함은 개방불가까지 3택이다', async () => {
      await openDetailWithMinor('초상권 포함')

      expect(screen.getByRole('radio', { name: '개방불가' })).toBeEnabled()
      expect(screen.getByRole('radio', { name: '신유형 개방' })).toBeDisabled()
      expect(screen.getByText('개방불가도 선택 가능')).toBeTruthy()

      fireEvent.click(screen.getByRole('radio', { name: '개방불가' }))
      expect(screen.getByRole('radio', { name: '개방불가' })).toBeChecked()
      expect(screen.getByRole('radio', { name: '공공누리 유형 개방불가' })).toBeChecked()
    })

    it('비고 문구는 여러 번 눌러도 한 번만 붙는다', async () => {
      await openDetailWithMinor('4. 권리 미보유', { koglType: '1유형' })

      fireEvent.click(screen.getByRole('radio', { name: '권리처리 추진 미희망' }))
      fireEvent.click(screen.getByRole('radio', { name: '권리처리 추진' }))
      fireEvent.click(screen.getByRole('radio', { name: '권리처리 추진 미희망' }))

      const note = (screen.getByLabelText('비고') as HTMLTextAreaElement).value
      expect(note.match(/권리처리 추진 미희망에 따른/g)).toHaveLength(1)
    })
  })

  it('수정을 저장하면 폼 값으로 updateReviewJudgment를 호출하고 목록으로 돌아간다', async () => {
    mocks.getReviewPage.mockResolvedValue(page())
    mocks.getReviewItem.mockResolvedValue(item({ reviewMajor: null, reviewResult: null }))
    mocks.updateReviewJudgment.mockResolvedValue(item({ reviewResult: '신유형 개방' }))

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

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '수정' }))
    await waitFor(() => expect(mocks.getReviewItem).toHaveBeenCalled())

    fireEvent.click(screen.getByRole('radio', { name: '만료 저작물' }))
    fireEvent.change(screen.getByLabelText('의견'), { target: { value: '이상 없음' } })
    fireEvent.click(screen.getByRole('button', { name: '수정' }))

    await waitFor(() =>
      expect(mocks.updateReviewJudgment).toHaveBeenCalledWith(1, 1, {
        hasAttachment: 'N',
        koglAttached: 'Y',
        koglType: '1유형',
        aiType: null,
        openable: 'Y',
        reviewMajor: '만료 저작물',
        reviewMinor: null,
        reviewResult: '신유형 개방',
        judgedKoglType: null,
        judgedAiType: null,
        opinion: '이상 없음',
        lawyerNote: null,
        needsProcessing: null,
      }),
    )

    // 저장 후 목록으로 복귀 - 업로드 버튼 등 목록 화면 요소가 다시 보인다
    await waitFor(() => expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy())
  })

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

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

    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: '수정' }))
    await waitFor(() => expect(mocks.getReviewItem).toHaveBeenCalled())

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

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