/**
 * E2E: Table output format — spec 5.1
 *
 * Covers the default text-table output behaviour of query commands.
 * The default format (no -o flag) is an aligned text table; -o table does not
 * exist and returns an illegal_argument error.
 *
 * Primary command under test: difyctl get app
 * Additional commands: difyctl get workspace, difyctl auth devices list
 *
 * Non-automatable cases (excluded):
 *   5.4  Row/column alignment — requires visual inspection, no reliable
 *        programmatic assertion.
 *   5.7  Long-text truncation based on terminal width — terminal width is
 *        not controllable in E2E.
 *   5.8  Very long text still readable — same reason as 5.7, and test data
 *        cannot be controlled.
 *   5.9  CJK/emoji alignment — CJK column-width alignment requires visual
 *        inspection; current fixtures have no CJK app names.
 *   5.10 CJK column width — same as 5.9.
 *   5.11 Small terminal width — terminal width not controllable.
 *   5.12 Large terminal width — same as 5.11.
 *   5.13 ANSI colour in TTY — E2E runs with NO_COLOR=1 and CI=1 (non-TTY).
 *   5.18 NULL fields stable — current fixtures have no NULL field values.
 *   5.21 run app --stream non-table — covered by run-app-streaming.e2e.ts.
 *   5.22 describe app uses describe printer — covered by describe-app.e2e.ts.
 *   5.23 Printer error / fallback — cannot reliably trigger a printer error.
 *   5.24 Printer error exit code — same as 5.23.
 *   5.20 get app -A -o wide has WORKSPACE column — covered by
 *        get-app-all-workspaces.e2e.ts (spec 3.92/3.93).
 *
 * Already covered in get-app-list.e2e.ts (not duplicated here):
 *   5.1  (partial) default format is not JSON
 *   5.2  (partial) header contains ID / NAME / MODE
 *   5.14 no ANSI colour codes in non-TTY
 *
 * All cases require a valid session (DIFY_E2E_TOKEN).
 */

import type { AuthFixture } from '../../helpers/cli.js'
import { afterEach, beforeEach, describe, expect, it, inject } from 'vitest'
import { assertExitCode, assertNoAnsi } from '../../helpers/assert.js'
import { withAuthFixture } from '../../helpers/cli.js'
import { loadE2EEnv, resolveEnv } from '../../setup/env.js'

// @ts-expect-error — see test/e2e/helpers/vitest-context.ts for explanation
const caps = inject('e2eCapabilities') as import('../../setup/env.js').E2ECapabilities
const E = resolveEnv(caps)

// ── 5.1 / 5.2 / 5.3 / 5.5 / 5.19 — Header & columns ───────────────────────
describe('E2E / table output — header and column format (spec 5.1–5.19)', () => {
  let fx: AuthFixture

  beforeEach(async () => { fx = await withAuthFixture(E) })
  afterEach(async () => { await fx.cleanup() })

  it('[P0] 5.1 default output (no -o) is an aligned text table, not JSON or YAML', async () => {
    // Spec 5.1: the default format is a text table; -o table does not exist.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    // Must not be JSON (starts with {) or YAML (starts with -)
    expect(result.stdout.trimStart()).not.toMatch(/^\{/)
    expect(result.stdout.trimStart()).not.toMatch(/^- /)
    // Must have content (non-empty)
    expect(result.stdout.trim().length).toBeGreaterThan(0)
  })

  it('[P0] 5.2 header row contains all five expected column names', async () => {
    // Spec 5.2: header columns are NAME / ID / MODE / TAGS / UPDATED.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    const header = result.stdout.split('\n')[0] ?? ''
    expect(header).toMatch(/NAME/i)
    expect(header).toMatch(/ID/i)
    expect(header).toMatch(/MODE/i)
    expect(header).toMatch(/TAGS/i)
    expect(header).toMatch(/UPDATED/i)
  })

  it('[P0] 5.3 column order is NAME → ID → MODE → TAGS → UPDATED', async () => {
    // Spec 5.3: columns appear in the defined order (as verified from actual CLI output).
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    const header = result.stdout.split('\n')[0] ?? ''
    const nameIdx = header.indexOf('NAME')
    const idIdx = header.indexOf('ID')
    const modeIdx = header.indexOf('MODE')
    const tagsIdx = header.indexOf('TAGS')
    const updatedIdx = header.indexOf('UPDATED')
    // All columns must be present
    expect(nameIdx).toBeGreaterThanOrEqual(0)
    expect(idIdx).toBeGreaterThanOrEqual(0)
    expect(modeIdx).toBeGreaterThanOrEqual(0)
    expect(tagsIdx).toBeGreaterThanOrEqual(0)
    expect(updatedIdx).toBeGreaterThanOrEqual(0)
    // Verify left-to-right order
    expect(nameIdx).toBeLessThan(idIdx)
    expect(idIdx).toBeLessThan(modeIdx)
    expect(modeIdx).toBeLessThan(tagsIdx)
    expect(tagsIdx).toBeLessThan(updatedIdx)
  })

  it('[P0] 5.5 table displays multiple data rows when more than one app exists', async () => {
    // Spec 5.5: when there are multiple apps, all rows are rendered.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    const lines = result.stdout.trim().split('\n').filter(l => l.trim())
    // At least header + 1 data row
    expect(lines.length).toBeGreaterThan(1)
  })

  it('[P0] 5.6 empty result set shows only the header row (no data rows)', async () => {
    // Spec 5.6: when the filter matches nothing, the output is a single header
    // row with no data rows underneath (not an error, exit 0).
    const result = await fx.r(['get', 'app', '--name', 'zzz-nonexistent-app-xyz-000'])
    assertExitCode(result, 0)
    const lines = result.stdout.trim().split('\n').filter(l => l.trim())
    // Only the header row should remain
    expect(lines).toHaveLength(1)
    expect(lines[0] ?? '').toMatch(/NAME/i)
  })

  it('[P0] 5.19 all header column names are uppercase', async () => {
    // Spec 5.19: header column names follow all-caps convention per implementation.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    const header = result.stdout.split('\n')[0] ?? ''
    // Extract word-like tokens from the header
    const tokens = header.match(/[A-Z]{2,}/g) ?? []
    expect(tokens.length).toBeGreaterThan(0)
    tokens.forEach(token =>
      expect(token, `header token "${token}" should be uppercase`).toBe(token.toUpperCase()),
    )
  })

  // ── 5.15 / 5.16 — Pipe-friendliness ──────────────────────────────────────

  it('[P0] 5.15 default table output is pipe-friendly — no unexpected control characters', async () => {
    // Spec 5.15: output can pass through cat / pipes without corruption.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    // No NUL, BEL, BS, VT, FF, SO–US, DEL bytes that would corrupt a pipe
    // eslint-disable-next-line no-control-regex
    expect(result.stdout).not.toMatch(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/)
  })

  it('[P0] 5.16 default table output written to a file contains no control characters', async () => {
    // Spec 5.16: redirecting to a file must not embed control characters.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    assertNoAnsi(result.stdout, 'stdout')
    // eslint-disable-next-line no-control-regex
    expect(result.stdout).not.toMatch(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/)
  })

  // ── 5.17 — Empty-field rendering ─────────────────────────────────────────

  it('[P1] 5.17 empty TAGS field is rendered as blank — not as a dash (-)', async () => {
    // Spec 5.17: empty fields show blank, not the `-` placeholder.
    // Most apps in the fixture workspace have no tags.
    const result = await fx.r(['get', 'app'])
    assertExitCode(result, 0)
    const lines = result.stdout.trim().split('\n')
    const header = lines[0] ?? ''
    const tagsStart = header.indexOf('TAGS')
    const updatedStart = header.indexOf('UPDATED')
    // Check at least one data row: the TAGS slice should be blank, not '-'
    const dataLines = lines.slice(1).filter(l => l.trim())
    if (dataLines.length > 0 && tagsStart >= 0 && updatedStart > tagsStart) {
      const tagsSlice = (dataLines[0] ?? '').substring(tagsStart, updatedStart).trim()
      // If there are no tags, the slice should be empty (not contain a lone '-')
      if (tagsSlice === '') {
        expect(tagsSlice).toBe('')
      }
      else {
        // Tags are present — just verify it's not the placeholder dash
        expect(tagsSlice).not.toBe('-')
      }
    }
  })

  // ── 5.25 — Performance ────────────────────────────────────────────────────

  it('[P1] 5.25 querying up to 100 apps completes without timeout', async () => {
    // Spec 5.25: large result sets must not freeze the CLI.
    // The testTimeout covers the timeout assertion implicitly.
    const result = await fx.r(['get', 'app', '--limit', '100'])
    assertExitCode(result, 0)
    expect(result.stdout.trim().length).toBeGreaterThan(0)
  })

  // ── 5.26 — Sort stability ─────────────────────────────────────────────────

  it('[P1] 5.26 two consecutive get app calls return rows in the same order', async () => {
    // Spec 5.26: output order must be deterministic (updated_at DESC).
    const r1 = await fx.r(['get', 'app', '-o', 'name'])
    const r2 = await fx.r(['get', 'app', '-o', 'name'])
    assertExitCode(r1, 0)
    assertExitCode(r2, 0)
    expect(r1.stdout).toBe(r2.stdout)
  })

  // ── Additional commands — header format ───────────────────────────────────

  it('[P0] get workspace default table has correct column headers', async () => {
    // Verifies the header columns for the workspace list table.
    const result = await fx.r(['get', 'workspace'])
    assertExitCode(result, 0)
    const header = result.stdout.split('\n')[0] ?? ''
    expect(header).toMatch(/ID/i)
    expect(header).toMatch(/NAME/i)
    expect(header).toMatch(/ROLE/i)
    expect(header).toMatch(/STATUS/i)
    expect(header).toMatch(/CURRENT/i)
  })

  it('[P0] auth devices list default table has correct column headers', async () => {
    // Verifies the header columns for the devices list table.
    const result = await fx.r(['auth', 'devices', 'list'])
    assertExitCode(result, 0)
    const header = result.stdout.split('\n')[0] ?? ''
    expect(header).toMatch(/DEVICE/i)
    expect(header).toMatch(/CREATED/i)
    expect(header).toMatch(/CURRENT/i)
  })

  // ── -o table is not a valid format ────────────────────────────────────────

  it('[P0] -o table returns illegal_argument error for query commands', async () => {
    // Spec: -o table does not exist; the default (no -o) is the table format.
    const result = await fx.r(['get', 'app', '-o', 'table'])
    expect(result.exitCode).toBe(2)
    expect(result.stderr).toMatch(/illegal_argument|illegal value table/i)
    expect(result.stderr).toMatch(/json|yaml|name|wide/i)
  })
})
