/**
 * importAnalyzerTool.test.js — coverage tests for the import analyzer.
 *
 * Strategy:
 *   - The tool wraps an internal ImportExportAnalyzer that traverses a
 *     real filesystem and parses real JS/TS source for imports/exports.
 *     Mocking the internal class would be hostile to refactoring and
 *     would test the wrong thing. Instead we build small fixture
 *     projects in a tmpDir and run the tool end-to-end against them.
 *   - Coverage gain: outer ImportAnalyzerTool (parse / validate / format
 *     / execute) AND most of the inner ImportExportAnalyzer (loadIgnore,
 *     getAllFiles, parseImports, parseExports, resolveImportPath,
 *     findCircularDependencies, findUnusedExports, analyze).
 */

import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
import os from 'os';
import fs from 'fs/promises';
import path from 'path';
import ImportAnalyzerTool from '../importAnalyzerTool.js';

// ───────────────────────── fixture builders ──────────────────────────

async function mkProject(name) {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), `loxia-importer-${name}-`));
  return dir;
}

async function writeFiles(dir, files) {
  for (const [rel, content] of Object.entries(files)) {
    const full = path.join(dir, rel);
    await fs.mkdir(path.dirname(full), { recursive: true });
    await fs.writeFile(full, content);
  }
}

// ───────────────────────────── identity ───────────────────────────────

describe('ImportAnalyzerTool — identity & description', () => {
  test('constructor wires the documented id', () => {
    const tool = new ImportAnalyzerTool();
    expect(tool.id).toBe('import-analyzer');
  });

  test('getDescription mentions the documented modes and outputs', () => {
    const d = new ImportAnalyzerTool().getDescription();
    expect(d).toMatch(/import-analyzer/);
    expect(d).toMatch(/full/);
    expect(d).toMatch(/quick/);
    expect(d).toMatch(/fix/);
    expect(d).toMatch(/summary/);
    expect(d).toMatch(/detailed/);
    expect(d).toMatch(/json/);
  });
});

// ───────────────────────── parseParameters ────────────────────────────

describe('ImportAnalyzerTool — parseParameters', () => {
  const tool = new ImportAnalyzerTool();

  test('accepts a JSON object and applies defaults', () => {
    expect(tool.parseParameters({})).toEqual({
      path: '.', mode: 'full', output: 'summary', ignoreFile: '.gitignore',
    });
  });

  test('JSON object overrides win', () => {
    expect(tool.parseParameters({ path: './src', mode: 'quick', output: 'detailed', ignoreFile: '.dockerignore' }))
      .toEqual({ path: './src', mode: 'quick', output: 'detailed', ignoreFile: '.dockerignore' });
  });

  test('modern XML format with attributes only (requires opening + closing tag)', () => {
    // The regex demands <import-analyzer ATTRS>...</import-analyzer>; a
    // self-closing form falls through to the (broken) legacy parser path.
    expect(tool.parseParameters('<import-analyzer path="./src" mode="quick" output="json" ignore-file=".npmignore"></import-analyzer>'))
      .toEqual({ path: './src', mode: 'quick', output: 'json', ignoreFile: '.npmignore' });
  });

  test('modern XML format with inner-content overriding attributes', () => {
    const xml = `<import-analyzer path="ignored">
      <path>./inner</path>
      <mode>fix</mode>
      <output>detailed</output>
      <ignore-file>.eslintignore</ignore-file>
    </import-analyzer>`;
    expect(tool.parseParameters(xml)).toMatchObject({
      path: './inner', mode: 'fix', output: 'detailed', ignoreFile: '.eslintignore',
    });
  });

  test('modern XML format with neither attrs nor inner content → defaults', () => {
    expect(tool.parseParameters('<import-analyzer></import-analyzer>')).toEqual({
      path: '.', mode: 'full', output: 'summary', ignoreFile: '.gitignore',
    });
  });

  test('legacy <analyze> path throws — TagParser has no parseTags(), so the fallback is unreachable', () => {
    // Documenting what actually happens: the modern regex doesn't match
    // <analyze>, the legacy try/catch swallows the resulting "not a
    // function" error, and the tool falls all the way through to the
    // "Invalid format" throw. If a real parseTags() ever lands, this
    // test should be replaced with a "legacy still works" check.
    expect(() => tool.parseParameters('<analyze path="./lib"></analyze>'))
      .toThrow(/Invalid import-analyzer format/);
  });

  test('throws on a string with no recognised tag', () => {
    expect(() => tool.parseParameters('just random text')).toThrow(/Invalid import-analyzer format/);
  });

  test('throws on a non-string / non-object input', () => {
    expect(() => tool.parseParameters(42)).toThrow(/Expected string \(XML\) or object \(JSON\)/);
    expect(() => tool.parseParameters(null)).toThrow(/Expected string \(XML\) or object \(JSON\)/);
  });
});

// ─────────────────────── _validateParameters ─────────────────────────

describe('ImportAnalyzerTool — _validateParameters', () => {
  const tool = new ImportAnalyzerTool();

  test('accepts a fully populated, valid object', () => {
    expect(() => tool._validateParameters({
      path: '.', mode: 'full', output: 'summary', ignoreFile: '.gitignore',
    })).not.toThrow();
  });

  test('rejects non-object', () => {
    expect(() => tool._validateParameters(null)).toThrow(/must be an object/);
    expect(() => tool._validateParameters('string')).toThrow(/must be an object/);
  });

  test('rejects non-string path', () => {
    expect(() => tool._validateParameters({ path: 42 })).toThrow(/path must be a string/);
  });

  test('rejects unknown mode', () => {
    expect(() => tool._validateParameters({ mode: 'turbo' })).toThrow(/Invalid mode: turbo/);
  });

  test('rejects unknown output', () => {
    expect(() => tool._validateParameters({ output: 'xml' })).toThrow(/Invalid output: xml/);
  });

  test('rejects non-string ignoreFile', () => {
    expect(() => tool._validateParameters({ ignoreFile: 42 })).toThrow(/ignoreFile must be a string/);
  });
});

// ─────────────────────── _resolveAndValidatePath ─────────────────────

describe('ImportAnalyzerTool — _resolveAndValidatePath', () => {
  const tool = new ImportAnalyzerTool();

  test('resolves a relative path against projectDir', () => {
    const resolved = tool._resolveAndValidatePath('./src', { projectDir: '/tmp/proj' });
    // Normalize for cross-platform path separators
    expect(resolved.replace(/\\/g, '/')).toMatch(/^\/tmp\/proj\/src$/i);
  });

  test('uses directoryAccess.workingDirectory when present', () => {
    const resolved = tool._resolveAndValidatePath('./x', {
      projectDir: '/ignored',
      directoryAccess: { workingDirectory: '/from/access' },
    });
    expect(resolved.replace(/\\/g, '/')).toMatch(/^\/from\/access\/x$/i);
  });

  test('falls back to process.cwd() when no dir info supplied', () => {
    const resolved = tool._resolveAndValidatePath('./x', {});
    expect(resolved.includes('x')).toBe(true);
  });

  test('blocks path traversal outside the working dir', () => {
    expect(() => tool._resolveAndValidatePath('../../escape', { projectDir: '/tmp/proj' }))
      .toThrow(/Path traversal detected/);
  });
});

// ───────────────────────── execute() integration ─────────────────────

describe('ImportAnalyzerTool — execute against real fixture projects', () => {
  let cleanProj, missingFileProj, missingExportProj, circularProj;

  beforeAll(async () => {
    // Project 1: nothing broken
    cleanProj = await mkProject('clean');
    await writeFiles(cleanProj, {
      'src/a.js': "import { greet } from './b.js';\nexport function callGreet() { return greet(); }\nexport default callGreet;\n",
      'src/b.js': "export function greet() { return 'hello'; }\n",
      'src/c.js': "import a from './a.js';\nimport * as ns from './b.js';\nconst { greet: g } = require('./b.js');\nexport default a;\n",
    });

    // Project 2: import points at a non-existent file
    missingFileProj = await mkProject('missing-file');
    await writeFiles(missingFileProj, {
      'src/a.js': "import { x } from './does-not-exist.js';\n",
      'src/b.js': "export const x = 1;\n",
    });

    // Project 3: imports a symbol that isn't exported
    missingExportProj = await mkProject('missing-export');
    await writeFiles(missingExportProj, {
      'src/a.js': "import { nope } from './b.js';\nimport b from './b.js';\n",
      'src/b.js': "export const yep = 1;\n", // no `nope`, no default
    });

    // Project 4: a → b → a cycle
    circularProj = await mkProject('circular');
    await writeFiles(circularProj, {
      'src/a.js': "import { b } from './b.js';\nexport const a = 1;\n",
      'src/b.js': "import { a } from './a.js';\nexport const b = 1;\n",
    });
  });

  afterAll(async () => {
    for (const d of [cleanProj, missingFileProj, missingExportProj, circularProj]) {
      try { await fs.rm(d, { recursive: true, force: true }); } catch { /* */ }
    }
  });

  test('clean project: success, no issues, summary output', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'full', output: 'summary', ignoreFile: '.gitignore' },
      { projectDir: cleanProj },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.issuesFound).toBe(0);
    expect(r.output).toMatch(/No import\/export issues detected/);
  });

  test('detects missing file imports', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'full', output: 'detailed', ignoreFile: '.gitignore' },
      { projectDir: missingFileProj },
    );
    expect(r.success).toBe(true);
    expect(r.results.fileNotFoundImports.length).toBeGreaterThan(0);
    expect(r.output).toMatch(/MISSING FILES/);
    expect(r.output).toMatch(/does-not-exist/);
  });

  test('detects missing exports and suggests available alternatives', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'full', output: 'detailed', ignoreFile: '.gitignore' },
      { projectDir: missingExportProj },
    );
    expect(r.success).toBe(true);
    const missing = r.results.missingExports;
    expect(Object.keys(missing).length).toBeGreaterThan(0);
    expect(r.output).toMatch(/MISSING EXPORTS/);
    expect(r.output).toMatch(/Available exports/);
  });

  test('detects circular dependencies in full mode', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'full', output: 'detailed', ignoreFile: '.gitignore' },
      { projectDir: circularProj },
    );
    expect(r.success).toBe(true);
    expect(r.results.circularDependencies.length).toBeGreaterThan(0);
    expect(r.output).toMatch(/CIRCULAR DEPENDENCIES/);
  });

  test('quick mode skips circular dependency detection', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'quick', output: 'detailed', ignoreFile: '.gitignore' },
      { projectDir: circularProj },
    );
    expect(r.success).toBe(true);
    // Quick mode does NOT populate circularDependencies.
    expect(r.results.circularDependencies).toBeUndefined();
  });

  test('json output returns parseable JSON (no header lines)', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'quick', output: 'json', ignoreFile: '.gitignore' },
      { projectDir: cleanProj },
    );
    expect(r.success).toBe(true);
    const parsed = JSON.parse(r.output);
    expect(parsed.summary.totalFiles).toBeGreaterThan(0);
  });

  test('returns structured error when path does not exist', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: './does-not-exist', mode: 'full', output: 'summary', ignoreFile: '.gitignore' },
      { projectDir: cleanProj },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/does not exist/i);
  });

  test('returns structured error on invalid mode (validation failure)', async () => {
    const r = await new ImportAnalyzerTool().execute(
      { path: '.', mode: 'BOGUS', output: 'summary', ignoreFile: '.gitignore' },
      { projectDir: cleanProj },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Invalid mode/);
  });

  test('respects ignore patterns from .gitignore', async () => {
    // Add a .gitignore and a file in the ignored dir; analyzer should skip it.
    const proj = await mkProject('ignored');
    try {
      await writeFiles(proj, {
        '.gitignore': 'ignored-dir\n',
        'src/keep.js': "import { x } from './x.js';\nexport const x = 1;\n",
        'src/x.js': "export const x = 1;\n",
        'ignored-dir/skip.js': "import nope from 'doesntmatter';\n",
      });
      const r = await new ImportAnalyzerTool().execute(
        { path: '.', mode: 'quick', output: 'json', ignoreFile: '.gitignore' },
        { projectDir: proj },
      );
      expect(r.success).toBe(true);
      const parsed = JSON.parse(r.output);
      // The "ignored-dir" file shouldn't have been parsed.
      const inImporting = JSON.stringify(parsed).includes('ignored-dir');
      expect(inImporting).toBe(false);
    } finally {
      try { await fs.rm(proj, { recursive: true, force: true }); } catch { /* */ }
    }
  });

  test('parses CommonJS require() statements (destructured + variable)', async () => {
    const proj = await mkProject('cjs');
    try {
      await writeFiles(proj, {
        'a.js': "const { hello } = require('./b.js');\nconst whole = require('./b.js');\nmodule.exports = { hello, whole };\n",
        'b.js': "exports.hello = 1;\nmodule.exports.bye = 2;\n",
      });
      const r = await new ImportAnalyzerTool().execute(
        { path: '.', mode: 'quick', output: 'json', ignoreFile: '.gitignore' },
        { projectDir: proj },
      );
      expect(r.success).toBe(true);
      const parsed = JSON.parse(r.output);
      expect(parsed.summary.totalImports).toBeGreaterThanOrEqual(2);
    } finally {
      try { await fs.rm(proj, { recursive: true, force: true }); } catch { /* */ }
    }
  });

  test('finds unused exports in full mode', async () => {
    const proj = await mkProject('unused');
    try {
      await writeFiles(proj, {
        'a.js': "import { used } from './b.js';\nconsole.log(used);\n",
        'b.js': "export const used = 1;\nexport const unused = 2;\n",
      });
      const r = await new ImportAnalyzerTool().execute(
        { path: '.', mode: 'full', output: 'json', ignoreFile: '.gitignore' },
        { projectDir: proj },
      );
      expect(r.success).toBe(true);
      const parsed = JSON.parse(r.output);
      expect(parsed.unusedExports).toBeDefined();
      // 'unused' should appear in the unused list for b.js
      const bUnused = Object.values(parsed.unusedExports).flat();
      expect(bUnused).toContain('unused');
    } finally {
      try { await fs.rm(proj, { recursive: true, force: true }); } catch { /* */ }
    }
  });
});
