import { expect, afterEach } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { parseXml } from '@usejunior/docx-core';

import { SessionManager } from '../session/manager.js';
import { openDocument } from '../tools/open_document.js';
import { readFile } from '../tools/read_file.js';
import {
  extractParaIdsFromToon,
  firstParaIdFromToon,
  makeMinimalDocx,
  makeDocxWithDocumentXml,
} from './docx_test_utils.js';
import { allureStep } from './allure-test.js';

// ---------------------------------------------------------------------------
// Tracked temp dirs with automatic cleanup
// ---------------------------------------------------------------------------

const trackedTempDirs: string[] = [];

export function registerCleanup(): void {
  afterEach(async () => {
    for (const dir of trackedTempDirs.splice(0)) {
      await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
    }
  });
}

export async function createTrackedTempDir(prefix = 'safe-docx-test-'): Promise<string> {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
  trackedTempDirs.push(dir);
  return dir;
}

// ---------------------------------------------------------------------------
// Session manager factory
// ---------------------------------------------------------------------------

export function createTestSessionManager(opts?: { ttlMs?: number }): SessionManager {
  return new SessionManager({ ttlMs: opts?.ttlMs ?? 60 * 60 * 1000 });
}

// ---------------------------------------------------------------------------
// openSession — one-shot open+read helper
// ---------------------------------------------------------------------------

interface OpenSessionOptions {
  xml?: string;
  extraFiles?: Record<string, string>;
  format?: 'toon' | 'simple' | 'json';
  mgr?: SessionManager;
  prefix?: string;
  trackOpenStep?: boolean;
}

interface OpenSessionResult {
  mgr: SessionManager;
  sessionId: string;
  filePath: string;
  content: string;
  paraIds: string[];
  firstParaId: string;
  tmpDir: string;
  inputPath: string;
}

export async function openSession(
  paragraphs: string[],
  opts?: OpenSessionOptions,
): Promise<OpenSessionResult> {
  const open = async () => {
    const mgr = opts?.mgr ?? createTestSessionManager();
    const tmpDir = await createTrackedTempDir(opts?.prefix);
    const inputPath = path.join(tmpDir, 'input.docx');

    const buf = opts?.xml
      ? await makeDocxWithDocumentXml(opts.xml, opts.extraFiles)
      : await makeMinimalDocx(paragraphs);
    await fs.writeFile(inputPath, new Uint8Array(buf));

    const opened = await openDocument(mgr, { file_path: inputPath });
    assertSuccess(opened, 'open');
    const sessionId = opened.session_id as string ?? '';
    const filePath = (opened.file_path as string) ?? inputPath;

    const read = await readFile(mgr, { file_path: inputPath, format: opts?.format });
    assertSuccess(read, 'read');
    const content = String(read.content);
    const paraIds = extractParaIdsFromToon(content);
    const firstParaId = paraIds.length > 0 ? paraIds[0]! : firstParaIdFromToon(content);

    return { mgr, sessionId, filePath, content, paraIds, firstParaId, tmpDir, inputPath };
  };

  if (opts?.trackOpenStep === false) {
    return open();
  }
  return allureStep('Open test session', open);
}

// ---------------------------------------------------------------------------
// Result assertion helpers
// ---------------------------------------------------------------------------

export function assertSuccess<T extends { success: boolean }>(
  result: T,
  label = 'operation',
): asserts result is T & { success: true } {
  if (!result.success) {
    // Surface the structured error so failures don't reduce to
    // "expected true received false". Tests hitting PATH_NOT_ALLOWED,
    // FILE_NOT_FOUND, etc. now show the exact reason in both the
    // assertion message and the thrown Error.
    const detail = describeFailure(result);
    expect.fail(`${label} failed: ${detail}`);
  }
  expect(result.success).toBe(true);
}

function describeFailure(result: unknown): string {
  if (!result || typeof result !== 'object') return String(result);
  const error = (result as { error?: unknown }).error;
  if (!error || typeof error !== 'object') {
    try {
      return JSON.stringify(result);
    } catch {
      return String(result);
    }
  }
  try {
    return JSON.stringify(error);
  } catch {
    return String(error);
  }
}

export function assertFailure<T extends { success: boolean; error?: { code?: string } }>(
  result: T,
  expectedCode?: string,
  label = 'operation',
): asserts result is T & { success: false } {
  expect(result.success).toBe(false);
  if (result.success) throw new Error(`expected ${label} to fail`);
  if (expectedCode) {
    expect(result.error?.code).toBe(expectedCode);
  }
}

// ---------------------------------------------------------------------------
// XML output parsing helper
// ---------------------------------------------------------------------------

interface ParsedOutputXml {
  dom: Document;
  runs: Element[];
  runText: (r: Element) => string;
  hasBold: (r: Element) => boolean;
  hasItalic: (r: Element) => boolean;
  hasUnderline: (r: Element) => boolean;
  hasHighlight: (r: Element) => boolean;
}

const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';

export async function parseOutputXml(outputPath: string): Promise<ParsedOutputXml> {
  const { readDocumentXmlFromPath } = await import('./docx_test_utils.js');
  const outXml = await readDocumentXmlFromPath(outputPath);
  const dom = parseXml(outXml);
  const runs = Array.from(dom.getElementsByTagNameNS(W_NS, 'r')) as Element[];

  const runText = (r: Element) =>
    Array.from(r.getElementsByTagNameNS(W_NS, 't'))
      .map((t) => t.textContent ?? '')
      .join('');

  const hasBold = (r: Element) => !!r.getElementsByTagNameNS(W_NS, 'b').item(0);

  const hasItalic = (r: Element) => !!r.getElementsByTagNameNS(W_NS, 'i').item(0);

  const hasUnderline = (r: Element) => {
    const rPr = r.getElementsByTagNameNS(W_NS, 'rPr').item(0) as Element | null;
    if (!rPr) return false;
    const u = rPr.getElementsByTagNameNS(W_NS, 'u').item(0) as Element | null;
    if (!u) return false;
    const val = u.getAttribute('w:val') ?? u.getAttribute('val');
    return val !== 'none';
  };

  const hasHighlight = (r: Element) => {
    const rPr = r.getElementsByTagNameNS(W_NS, 'rPr').item(0) as Element | null;
    if (!rPr) return false;
    return !!rPr.getElementsByTagNameNS(W_NS, 'highlight').item(0);
  };

  return { dom, runs, runText, hasBold, hasItalic, hasUnderline, hasHighlight };
}
