import { expect, test, type Page } from '@playwright/test'
import { Ticket04Stack } from './support/ticket04-stack'

const stack = new Ticket04Stack()
const kinds = ['compare', 'formation', 'splice'] as const
type Kind = typeof kinds[number]
const sources = [9301, 9302, 9303, 9304]
const selection = (id: number, battery = id) => ({ testId: id, testBatteryId: battery })
const compareSelections = [9301, 9303].flatMap(id => [1, 3].map(cycleNo => ({ ...selection(id), cycleNo, stepNo: 1, logicalNo: null })))
const colors = ['#123456', '#3978f6', '#f08a3e', '#109854']

test.use({ actionTimeout: 20_000 })
test.beforeAll(async () => { await stack.start(); seed() })
test.afterAll(async () => stack.stop())

async function login(page: Page, username = 'admin') {
  await page.goto('/login')
  await page.evaluate(() => {
    localStorage.removeItem('battery_detection_auth_token')
    localStorage.removeItem('battery_detection_auth_user')
  })
  await page.goto('/login')
  await page.locator('input[autocomplete="username"]').fill(username)
  await page.locator('input[autocomplete="current-password"]').fill(username === 'admin' ? 'admin123' : 'Report123!')
  await page.getByRole('button', { name: '登录', exact: true }).click()
  await expect(page).toHaveURL(/\/dashboard$/)
}

async function request(page: Page, path: string, method = 'GET', data?: unknown) {
  const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token'))
  return (await page.request.fetch(`${stack.backendUrl}/system/${path}`, {
    method, data, headers: { Authorization: `Bearer ${token}` },
  })).json()
}

async function api(page: Page, path: string, method = 'GET', data?: unknown) {
  const body = await request(page, path, method, data)
  expect(body.code, `${path}: ${body.msg}`).toBe(200)
  return body
}

function payload(kind: Kind, name: string) {
  const settings = kind === 'compare'
    ? { metrics: ['voltage', 'current', 'temperature'], seriesColors: colors, batteryTemplateFields: ['F'], specialFields: [], detailFieldValues: {} }
    : kind === 'formation'
      ? { fields: [{ key: 'chargeCapacity', scope: 'cycle', kind: 'metric', displayName: '充电容量', unit: 'mAh' }], values: {}, chargeStepSelections: {}, metrics: ['voltage', 'current', 'temperature'], seriesColors: {} }
      : { batteryFields: [], specialFields: [], detailFieldValues: {}, metrics: ['finalVoltage1', 'energyEfficiency'], seriesColors: colors }
  return { name, description: 'Ticket13 隔离受控来源', ...(kind === 'compare' ? { experimentName: '' } : {}), settings,
    selections: kind === 'compare' ? compareSelections : (kind === 'formation' ? sources : [9301, 9303]).map(id => selection(id)) }
}

// 仅向本次临时库导入确定的完成态事实；不连接设备，不以此证明真实协议兼容。
function seed() {
  stack.executeIsolatedSql(`
    INSERT INTO t_battery_group(group_id,group_name,model) VALUES(9301,'连续验收组','验收型号'),(9304,'异组同名','验收型号');
    INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value)
      SELECT 9301,field_id,'12.5' FROM t_battery_group_field WHERE field_name='F';
    INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(9301,'化成');
    INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status,started_at_utc)
      VALUES ${[...sources, 9305].map((id, i) => `(${id},'连续来源${i + 1}',1,1,9301,'隔离受控导入','${id === 9305 ? 'WAITING' : 'COMPLETED'}','${id === 9305 ? 'NOT_STARTED' : 'ENDED'}','2026-09-0${i + 1} 00:00:00')`).join(',')};
    INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id)
      VALUES ${[...sources, 9305].map(id => `(${id},${id},${id === 9304 ? 9304 : 9301},'同名电池',${id})`).join(',')},
      (9401,9301,9301,'第二电池',9401),(9402,9302,9301,'第二电池',9402),(9403,9303,9301,'第二电池',9403);
  `)
  for (const battery of [...sources, 9401, 9402, 9403]) {
    const id = battery >= 9401 ? battery - 100 : battery
    const cycles = id === 9302 ? [5, 7] : id === 9303 ? [1, 3, 5] : [1, 3]
    stack.executeIsolatedSql(`
      INSERT INTO t_test_realtime_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,cabinet_devtype,cabinet_devid,cabinet_subdevid,cabinet_chlid,cabinet_sequence_id,cabinet_device_time_utc,collector_device_time_utc,server_received_at_utc,cycle_no,step_no,step_name,voltage_v,current_a,temperature,positive_potential,negative_potential,content_sha256)
        VALUES ${cycles.flatMap(cycle => [0, 1, 2].map(point => `(${id},${battery},${battery},'acceptance','acceptance:${battery}','${cycle * 10 + point}',27,1,1,${battery},${cycle * 10 + point},'2026-09-01 0${cycle}:0${point === 2 ? 4 : point}:10.123','2026-09-01 0${cycle}:0${point === 2 ? 4 : point}:00.123','2026-09-01 0${cycle}:0${point === 2 ? 4 : point}:11.123',${cycle},1,'实际充电',3.7,${point === 0 ? '-8.1651' : point === 1 ? 'NULL' : '0'},0,0.1,-0.2,UNHEX(SHA2('${battery}:${cycle}:${point}',256)))`)).join(',')};
      INSERT INTO t_test_step_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,cycle_no,step_no,step_name,step_type,device_time_utc,server_received_at_utc,adapter_timezone_id,device_time_raw,voltage_v,content_sha256)
        VALUES ${cycles.flatMap(cycle => [1, 2].map(step => `(${id},${battery},${battery},'acceptance','acceptance:${battery}','${cycle * 10 + step}','${cycle * 10 + step}',${cycle},${step},'${step === 1 ? '实际充电' : '实际放电'}','${step === 1 ? 'cc' : 'dc'}','2026-09-01 0${cycle}:0${step}:00.123','2026-09-01 0${cycle}:0${step}:01.123','Asia/Hong_Kong','2026-09-01 08:00:00.123',${cycle === 3 && step === 2 ? 'NULL' : step === 1 ? '4.2' : '0'},UNHEX(SHA2('${battery}:${cycle}:${step}',256)))`)).join(',')};
      INSERT INTO t_test_cycle_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,cycle_no,device_time_utc,server_received_at_utc,adapter_timezone_id,device_time_raw,charge_capacity_ah,discharge_capacity_ah,charge_energy_wh,discharge_energy_wh,content_sha256)
        VALUES ${cycles.map(cycle => `(${id},${battery},${battery},'acceptance','acceptance:${battery}','${cycle}','${cycle}',${cycle},'2026-09-01 0${cycle}:03:00.123','2026-09-01 0${cycle}:03:01.123','Asia/Hong_Kong','2026-09-01 08:00:00.123',8.5122,0,${cycle === 3 ? '0' : '0.1361'},0.1694,UNHEX(SHA2('${battery}:${cycle}',256)))`).join(',')};
    `)
  }
}

test('同一环境连续保存三类报表，三子页修订、失败回滚、重启及删源均保持版本独立', async ({ page }) => {
  await login(page)
  const saved: Partial<Record<Kind, any>> = {}
  for (const kind of kinds) {
    const candidates = (await api(page, `${kind}-report/sources`)).data
    expect(candidates.some((row: any) => row.selection.testId === 9305)).toBe(false)
    const draft = payload(kind, `连续验收-${kind}`)
    await api(page, `${kind}-report/preview`, 'POST', draft.selections)
    saved[kind] = (await api(page, `${kind}-report`, 'POST', draft)).data
    expect(saved[kind].versionNo).toBe(1)
  }
  expect(saved.compare.items).toHaveLength(4)
  expect(saved.compare.items[0].businessFields.find((field: any) => field.fieldName === 'F').fieldValue).toBe('12.5')
  expect(saved.compare.items[0].zeroTime).toBe(Date.parse('2026-09-01T01:00:00.123Z'))
  expect(saved.formation.cycleNos).toEqual([1, 3, 5, 7])
  expect(saved.formation.items[0].cycleNos).toEqual([1, 3])
  expect(saved.splice.segments[0].statistics['1'].energyEfficiency).toBeCloseTo(124.467303453, 6)

  for (const [kind, endpoint, field, value] of [
    ['REALTIME', 'records', 'current', -2], ['STEP', 'steps', 'endVoltage', 5.111], ['CYCLE', 'cycles', 'chargeCapacity', 9.7178],
  ] as const) {
    const rows = (await api(page, `test/9301/data/${endpoint}`)).data
    const row = rows.find((item: any) => item.cycleNo === 1 && item.batteryNo === '同名电池' && (kind !== 'STEP' || item.stepNo === 2))
    expect(row).toBeTruthy()
    await api(page, 'test/9301/console-data/actions', 'POST', {
      dataType: kind, operationType: 'EDIT', scopeSummary: `${kind} 连续验收修订`, reason: '核对三类报表版本独立',
      identitySummary: JSON.stringify([row.stableIdentity]),
      overrides: [{ stableIdentity: row.stableIdentity, fieldValues: JSON.stringify({ [field]: value }) }],
    })
    expect((await api(page, `test/9301/data/${endpoint}`)).data.find((item: any) => item.stableIdentity === row.stableIdentity)[field]).toBe(value)
  }
  stack.executeIsolatedSql("UPDATE t_battery_group_relation_field SET field_value='25' WHERE group_id=9301")
  const refreshed: Partial<Record<Kind, any>> = {}
  for (const kind of kinds) {
    const first = saved[kind]
    const path = `${kind}-report/${first.id}`
    expect((await api(page, path)).data).toEqual(first)
    const edit = { ...payload(kind, `连续编辑-${kind}`), selections: undefined, baseVersionNo: 1, changeSummary: '仅调整名称' }
    // 约束作用于真实写库过程，验证版本插入后失败会回滚，不模拟成功响应。
    stack.executeIsolatedSql(`ALTER TABLE t_${kind}_report ADD CONSTRAINT ticket13_save_failure CHECK (report_name <> 'failure')`)
    try { expect((await request(page, `${path}/versions`, 'POST', { ...edit, name: 'failure' })).code).not.toBe(200) }
    finally { stack.executeIsolatedSql(`ALTER TABLE t_${kind}_report DROP CONSTRAINT ticket13_save_failure`) }
    expect((await api(page, `${path}/versions`)).data).toHaveLength(1)
    expect((await api(page, path)).data).toEqual(first)
    const edited = (await api(page, `${path}/versions`, 'POST', edit)).data
    expect(kind === 'splice' ? edited.segments : edited.items).toEqual(kind === 'splice' ? first.segments : first.items)
    refreshed[kind] = (await api(page, `${path}/refresh-source`, 'POST', { baseVersionNo: 2, changeSummary: '主动读取三子页修订' })).data
    expect((await api(page, `${path}/versions/1`)).data).toEqual(first)
    for (const other of kinds.filter(item => item !== kind)) {
      expect((await api(page, `${other}-report/${saved[other].id}/versions/1`)).data).toEqual(saved[other])
    }
  }
  expect(refreshed.compare.items[0].records.some((row: any) => row.current === -2)).toBe(true)
  expect(refreshed.compare.items[0].businessFields.find((field: any) => field.fieldName === 'F').fieldValue).toBe('25')
  expect((await api(page, `compare-report/${saved.compare.id}/versions/1`)).data.items[0].businessFields.find((field: any) => field.fieldName === 'F').fieldValue).toBe('12.5')
  expect(refreshed.formation.items[0].cycles.find((row: any) => row.cycleNo === 1).chargeCapacity).toBe(9.7178)
  expect(refreshed.splice.segments[0].statistics['1'].finalVoltage1).toBe(5.111)
  await stack.restartBackend()
  await login(page)
  const errors: string[] = []
  page.on('pageerror', error => errors.push(error.message))
  for (const kind of kinds) {
    expect((await api(page, `${kind}-report/${saved[kind].id}`)).data).toEqual(refreshed[kind])
    await page.goto(`/report-${kind}/${saved[kind].id}/edit?mode=view`)
    await page.reload()
    if (kind === 'splice') await expect(page.locator('.splice-snapshot h1')).toHaveText(`连续编辑-${kind}`)
    else await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue(`连续编辑-${kind}`)
    await expect(page.getByRole('button', { name: '历史版本', exact: true })).toBeEnabled()
  }
  const logsBefore = stack.queryDatabase('SELECT COUNT(*) FROM sys_oper_log')
  await api(page, 'test/9301', 'DELETE')
  expect(stack.queryDatabase('SELECT COUNT(*) FROM sys_oper_log')).toBe(logsBefore)
  for (const table of ['t_test', 't_test_battery', 't_test_realtime_record', 't_test_step_record', 't_test_cycle_record', 't_test_data_revision_action']) {
    expect(stack.queryDatabase(`SELECT COUNT(*) FROM ${table} WHERE test_id=9301`)).toBe('0')
  }
  await page.context().addInitScript(() => { window.print = () => { document.body.dataset.printRequested = 'true' } })
  for (const kind of kinds) {
    const path = `${kind}-report/${saved[kind].id}`
    expect((await api(page, `${path}/versions/1/print`)).data).toEqual(saved[kind])
    expect((await request(page, `${path}/refresh-source`, 'POST', { baseVersionNo: 3, changeSummary: '已删除来源' })).code).not.toBe(200)
    const edited = (await api(page, `${path}/versions`, 'POST', {
      ...payload(kind, `删源编辑-${kind}`), selections: undefined, baseVersionNo: 3, changeSummary: '删源后保留快照',
    })).data
    expect(kind === 'splice' ? edited.segments : edited.items).toEqual(kind === 'splice' ? refreshed[kind].segments : refreshed[kind].items)
    await page.goto(`/report-${kind}/${saved[kind].id}/edit?mode=view&version=1`)
    for (const width of [1440, 390]) {
      await page.setViewportSize({ width, height: 900 })
      await expect(page.getByRole('button', { name: '打印 / 另存 PDF', exact: true })).toBeVisible()
      await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
      await page.screenshot({ path: `tests/e2e/.artifacts/ticket13-${kind}-${width}.png`, fullPage: true })
    }
    const popupPromise = page.waitForEvent('popup')
    await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click()
    const popup = await popupPromise
    await expect(popup.locator('body')).toHaveAttribute('data-print-requested', 'true')
    await expect(popup.locator('body')).toContainText(`连续验收-${kind}`)
    await popup.pdf({ path: `tests/e2e/.artifacts/ticket13-${kind}.pdf`, preferCSSPageSize: true, printBackground: true })
    await popup.close()
    expect((await api(page, `${path}/versions`)).data).toHaveLength(4)
    await page.evaluate(() => { window.open = () => null })
    await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click()
    await expect(page.getByText('打印窗口打开失败，请允许浏览器弹出窗口后重试', { exact: true })).toBeVisible()
  }
  const retainedSource = (await api(page, 'test/9303/data/cycles')).data
  for (const kind of kinds) {
    const path = `${kind}-report/${saved[kind].id}`
    await api(page, path, 'DELETE')
    expect(stack.queryDatabase(`SELECT COUNT(*) FROM t_${kind}_report_version WHERE report_id=${saved[kind].id}`)).toBe('0')
    expect((await request(page, `${path}/versions/1`)).code).not.toBe(200)
    const log = JSON.parse(stack.queryDatabase(`SELECT oper_param FROM sys_oper_log WHERE oper_url='/system/${path}' AND business_type=3`))
    expect(Object.keys(log).sort()).toEqual(['operatorId', 'reportId', 'reportName'])
    expect(log.reportId).toBe(saved[kind].id)
    expect((await api(page, 'test/9303/data/cycles')).data).toEqual(retainedSource)
    for (const other of kinds.slice(kinds.indexOf(kind) + 1)) {
      expect((await api(page, `${other}-report/${saved[other].id}/versions/1`)).data).toEqual(saved[other])
    }
  }
  expect(errors).toEqual([])
})

test('拼接前置行为：共同多电池、跳号和末工步缺失', async ({ page }) => {
  await login(page)
  // 独立测试使用未被连续演示删除的来源，并覆盖非 1 起始循环。
  const single = [selection(9302), selection(9303)]
  const preview = (await api(page, 'splice-report/preview', 'POST', single)).data
  expect.soft(preview.cycleNos, '应从 1 接续并保留段内跳号').toEqual([1, 3, 4, 6, 8])
  expect.soft(preview.segments[1].statistics['3'].finalVoltage1 ?? null, '最后实际工步缺失，不能回退到 4.2 V').toBeNull()
  const multiple = await request(page, 'splice-report/preview', 'POST', [selection(9302), selection(9302, 9402), selection(9303), selection(9303, 9403)])
  expect.soft(multiple.code, `共同多电池：${multiple.msg}`).toBe(200)
  const mismatch = await request(page, 'splice-report/preview', 'POST', [selection(9302), selection(9304)])
  expect(mismatch.code).not.toBe(200)
})

test('删除日志写入失败必须保留整份拼接报表和所有版本', async ({ page }) => {
  await login(page)
  const single = [selection(9302), selection(9303)]
  const created = (await api(page, 'splice-report', 'POST', { ...payload('splice', '删除失败保留完整报表'), selections: single })).data
  const edited = (await api(page, `splice-report/${created.id}/versions`, 'POST', {
    name: '删除失败保留两个版本', description: '', settings: created.settings, baseVersionNo: 1, changeSummary: '追加历史用于核对完整回滚',
  })).data
  stack.executeIsolatedSql(`ALTER TABLE sys_oper_log ADD CONSTRAINT ticket13_delete_failure CHECK (oper_url <> '/system/splice-report/${created.id}')`)
  try {
    expect((await request(page, `splice-report/${created.id}`, 'DELETE')).code).not.toBe(200)
    const reopened = await request(page, `splice-report/${created.id}`)
    expect(reopened.code, '日志插入失败必须回滚整份删除').toBe(200)
    expect(reopened.data).toEqual(edited)
    expect((await api(page, `splice-report/${created.id}/versions/1`)).data).toEqual(created)
    expect((await api(page, `splice-report/${created.id}/versions`)).data).toHaveLength(2)
    await page.goto('/report-splice')
    const row = page.locator('tbody tr').filter({ hasText: edited.name })
    await row.getByRole('button', { name: '删除', exact: true }).click()
    const rejected = page.waitForResponse(response => response.url().endsWith(`/system/splice-report/${created.id}`) && response.request().method() === 'DELETE')
    await page.getByRole('dialog', { name: '确认删除拼接报表', exact: true }).getByRole('button', { name: '删除', exact: true }).click()
    expect((await (await rejected).json()).code).not.toBe(200)
    await expect(page.locator('.el-message--error')).toBeVisible()
    await expect(page.getByText('拼接报表已删除', { exact: true })).toHaveCount(0)
    await expect(row).toBeVisible()
  } finally { stack.executeIsolatedSql('ALTER TABLE sys_oper_log DROP CONSTRAINT ticket13_delete_failure') }
  await api(page, `splice-report/${created.id}`, 'DELETE')
  expect((await request(page, `splice-report/${created.id}`)).code).not.toBe(200)
  expect(stack.queryDatabase(`SELECT COUNT(*) FROM t_splice_report_version WHERE report_id=${created.id}`)).toBe('0')
  expect(stack.queryDatabase(`SELECT COUNT(*) FROM sys_oper_log WHERE oper_url='/system/splice-report/${created.id}' AND business_type=3`)).toBe('1')
})

test('各类五动作权限可配置且不能跨类别使用，查看用户只能查看跨创建人报表', async ({ page }) => {
  await login(page)
  const modules = (await api(page, 'menu/permissionList')).data.flatMap((module: any) => module.pages)
  const names = { compare: '对比报表', formation: '化成报表', splice: '拼接报表' }
  const saved: Partial<Record<Kind, any>> = {}
  for (const kind of kinds) {
    saved[kind] = (await api(page, `${kind}-report`, 'POST', {
      ...payload(kind, `权限验收-${kind}`),
      selections: kind === 'compare' ? compareSelections.filter(item => item.testId === 9303) : [selection(9302), selection(9303)],
    })).data
    const permissions = modules.find((module: any) => module.pageName === names[kind]).permissions
    for (const action of ['查看', '新增', '编辑', '删除', '导出']) {
      expect.soft(permissions.filter((item: any) => item.menuName === action), `${names[kind]} ${action} 必须可通过正式权限页配置`).toHaveLength(1)
    }
    const username = `ticket13_${kind}`
    await api(page, 'role', 'POST', { roleName: username, roleKey: 'common', roleSort: 1, status: '0', menuIds: permissions.filter((item: any) => item.menuName === '查看').map((item: any) => item.menuId) })
    const role = (await api(page, `role/list?roleName=${username}`)).rows[0]
    await api(page, 'user', 'POST', { userName: username, nickName: username, password: 'Report123!', status: '0', roleIds: [role.roleId] })
  }
  for (const kind of kinds) {
    await login(page, `ticket13_${kind}`)
    await api(page, `${kind}-report/list`)
    expect((await api(page, `${kind}-report/${saved[kind].id}`)).data).toEqual(saved[kind])
    for (const target of kinds) {
      if (target !== kind) expect((await request(page, `${target}-report/list`)).code).toBe(403)
      for (const [path, method, data] of [
        [`${target}-report`, 'POST', payload(target, '无权限新建')],
        [`${target}-report/1/versions`, 'POST', { ...payload(target, '无权限编辑'), selections: undefined, baseVersionNo: 1, changeSummary: '拒绝' }],
        [`${target}-report/1`, 'DELETE', undefined],
        [`${target}-report/1/versions/1/print`, 'GET', undefined],
      ] as const) expect((await request(page, path, method, data)).code, path).toBe(403)
    }
    await page.goto(`/report-${kind}`)
    await expect(page.getByRole('button', { name: /新建|新增/ })).toHaveCount(0)
  }
})
