import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ProjectsPage from './ProjectsPage'
import { withCodes } from '../codes/fixtures'
import type { ContactLog, DashboardOrgRow, OrgReport, ProjectRow } from '../api/client'

const mocks = vi.hoisted(() => ({
  getProjects: vi.fn(),
  getContactLogs: vi.fn(),
  addContactLog: vi.fn(),
  updateContactLog: vi.fn(),
  deleteContactLog: vi.fn(),
  getReports: vi.fn(),
  uploadReport: vi.fn(),
  deleteReport: vi.fn(),
}))

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

function orgRow(overrides: Partial<DashboardOrgRow> = {}): DashboardOrgRow {
  return {
    id: 1,
    orgNo: '001_00',
    orgName: '국제방송교류재단',
    stage: 5,
    hasChannel: true,
    lawyerName: '권홍철',
    mjName: '이연경',
    lawyerAssignedDate: null,
    reviewTotal: 100,
    reviewDone: 40,
    processTotal: 10,
    processDone: 2,
    stageChangedAt: null,
    lastChangedAt: null,
    reCount: 0,
    ...overrides,
  }
}

function row(overrides: Partial<ProjectRow> = {}): ProjectRow {
  return {
    org: orgRow(),
    lastContactedOn: null,
    contactCount: 0,
    reportCount: 0,
    ...overrides,
  }
}

function log(overrides: Partial<ContactLog> = {}): ContactLog {
  return {
    id: 1,
    orgId: 1,
    contactedOn: '2026-07-20',
    method: '전화',
    summary: '신청 확인 통화',
    author: 'admin',
    createdAt: Date.now(),
    ...overrides,
  }
}

function report(overrides: Partial<OrgReport> = {}): OrgReport {
  return {
    id: 1,
    orgId: 1,
    fileName: '최종보고서.pdf',
    contentType: 'application/pdf',
    byteSize: 2048,
    uploadedBy: 'admin',
    uploadedAt: Date.now(),
    ...overrides,
  }
}

async function openPanel() {
  render(withCodes(<ProjectsPage onOpenOrg={vi.fn()} />))
  await screen.findByTestId('project-table')
  fireEvent.click(screen.getByRole('button', { name: '연락·보고서' }))
  await waitFor(() => expect(mocks.getContactLogs).toHaveBeenCalledWith(1))
}

describe('ProjectsPage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mocks.getProjects.mockResolvedValue([row()])
    mocks.getContactLogs.mockResolvedValue([])
    mocks.getReports.mockResolvedValue([])
  })

  it('기관 현황을 표로 보여준다', async () => {
    mocks.getProjects.mockResolvedValue([
      row({ lastContactedOn: '2026-07-20', contactCount: 3, reportCount: 1 }),
    ])
    render(withCodes(<ProjectsPage onOpenOrg={vi.fn()} />))

    const table = within(await screen.findByTestId('project-table'))
    expect(table.getByText('국제방송교류재단')).toBeTruthy()
    // 단계 태그는 번호와 이름이 각각 다른 span이다(번호는 tabular-nums로 세로 정렬된다).
    expect(table.getByText('5.')).toBeTruthy()
    expect(table.getByText('법률검토(권리확인)')).toBeTruthy()
    expect(table.getByText('2026-07-20')).toBeTruthy()
    expect(table.getByText('(3)')).toBeTruthy()
  })

  it('기관명을 클릭하면 기관관리로 넘긴다', async () => {
    const onOpenOrg = vi.fn()
    render(withCodes(<ProjectsPage onOpenOrg={onOpenOrg} />))

    fireEvent.click(await screen.findByRole('button', { name: '국제방송교류재단' }))
    expect(onOpenOrg).toHaveBeenCalledWith(1)
  })

  it('검색과 연락 이력 필터로 목록을 거른다', async () => {
    mocks.getProjects.mockResolvedValue([
      row(),
      row({ org: orgRow({ id: 2, orgNo: '002_00', orgName: '세종학당재단' }), contactCount: 2 }),
    ])
    render(withCodes(<ProjectsPage onOpenOrg={vi.fn()} />))
    await screen.findByTestId('project-table')

    fireEvent.change(screen.getByLabelText('기관명/기관코드 검색'), { target: { value: '세종' } })
    expect(within(screen.getByTestId('project-table')).queryByText('국제방송교류재단')).toBeNull()

    fireEvent.change(screen.getByLabelText('기관명/기관코드 검색'), { target: { value: '' } })
    fireEvent.change(screen.getByLabelText('연락 이력'), { target: { value: 'none' } })
    expect(within(screen.getByTestId('project-table')).queryByText('세종학당재단')).toBeNull()
    expect(within(screen.getByTestId('project-table')).getByText('국제방송교류재단')).toBeTruthy()
  })

  it('연락·보고서 창을 열면 이력과 보고서를 함께 읽는다', async () => {
    mocks.getContactLogs.mockResolvedValue([log()])
    mocks.getReports.mockResolvedValue([report()])

    await openPanel()

    expect(screen.getByText('신청 확인 통화')).toBeTruthy()
    expect(screen.getByText('최종보고서.pdf')).toBeTruthy()
    expect(screen.getByText(/2026-07-20 · 전화 · admin/)).toBeTruthy()
  })

  it('연락 이력을 등록하면 서버가 준 목록으로 갱신한다', async () => {
    mocks.addContactLog.mockResolvedValue([log({ summary: '자료 요청' })])
    await openPanel()

    fireEvent.change(screen.getByLabelText('연락일'), { target: { value: '2026-07-25' } })
    fireEvent.change(screen.getByLabelText('연락 방법'), { target: { value: '메일' } })
    fireEvent.change(screen.getByLabelText('연락 내용'), { target: { value: '자료 요청' } })
    fireEvent.click(screen.getByRole('button', { name: '등록' }))

    await waitFor(() =>
      expect(mocks.addContactLog).toHaveBeenCalledWith(1, {
        contactedOn: '2026-07-25',
        method: '메일',
        summary: '자료 요청',
      }),
    )
    expect(await screen.findByText('자료 요청')).toBeTruthy()
    // 목록 숫자(최근 연락)도 다시 읽는다
    expect(mocks.getProjects).toHaveBeenCalledTimes(2)
  })

  it('내용이 비면 등록 버튼이 잠긴다', async () => {
    await openPanel()
    expect(screen.getByRole('button', { name: '등록' })).toBeDisabled()
  })

  it('수정을 누르면 입력칸에 값이 올라오고 저장하면 updateContactLog를 부른다', async () => {
    mocks.getContactLogs.mockResolvedValue([log({ id: 9, summary: '원래 내용' })])
    mocks.updateContactLog.mockResolvedValue([log({ id: 9, summary: '고친 내용' })])
    await openPanel()

    fireEvent.click(screen.getByRole('button', { name: '수정' }))
    expect((screen.getByLabelText('연락 내용') as HTMLInputElement).value).toBe('원래 내용')

    fireEvent.change(screen.getByLabelText('연락 내용'), { target: { value: '고친 내용' } })
    fireEvent.click(screen.getByRole('button', { name: '수정 저장' }))

    await waitFor(() =>
      expect(mocks.updateContactLog).toHaveBeenCalledWith(1, 9, {
        contactedOn: '2026-07-20',
        method: '전화',
        summary: '고친 내용',
      }),
    )
  })

  it('연락 이력 삭제는 확인을 물어본다', async () => {
    mocks.getContactLogs.mockResolvedValue([log({ id: 9 })])
    mocks.deleteContactLog.mockResolvedValue(undefined)
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
    await openPanel()

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

    expect(confirmSpy).toHaveBeenCalled()
    await waitFor(() => expect(mocks.deleteContactLog).toHaveBeenCalledWith(1, 9))
    confirmSpy.mockRestore()
  })

  it('보고서를 고르면 업로드하고 목록을 갱신한다', async () => {
    mocks.uploadReport.mockResolvedValue([report({ fileName: '2차보고서.pdf' })])
    await openPanel()

    const file = new File(['내용'], '2차보고서.pdf', { type: 'application/pdf' })
    fireEvent.change(screen.getByLabelText('보고서 파일'), { target: { files: [file] } })

    await waitFor(() => expect(mocks.uploadReport).toHaveBeenCalledWith(1, file))
    expect(await screen.findByText('2차보고서.pdf')).toBeTruthy()
  })

  it('보고서는 다운로드 링크로 걸린다', async () => {
    mocks.getReports.mockResolvedValue([report({ id: 7 })])
    await openPanel()

    expect(screen.getByRole('link', { name: '최종보고서.pdf' })).toHaveAttribute(
      'href',
      '/api/orgs/1/reports/7',
    )
  })
})
