/**
 * dependencyResolverTool.test.js — coverage tests.
 *
 * Strategy:
 *   - The tool's semver logic is pure (parse/compare/range/complex) and
 *     therefore the cheapest, most valuable thing to cover thoroughly.
 *     ~half the file is that logic; we exercise it exhaustively.
 *   - Parameter parsing + validation + path resolution are also pure
 *     and cheap.
 *   - execute() requires real fetch + real npm exec; we mock global
 *     fetch and the child_process exec at the module boundary. Tests
 *     cover the "no package.json", "no deps", "check mode reports
 *     updates", and "fix mode with mocked install" paths.
 *
 * Mock note: `child_process.exec` is mocked via jest.unstable_mockModule
 * BEFORE importing the tool, the same pattern the existing
 * agentCommunicationTool tests use.
 */

import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach, jest } from '@jest/globals';
import os from 'os';
import fs from 'fs/promises';
import path from 'path';

const execMock = jest.fn();
jest.unstable_mockModule('child_process', () => ({
  exec: (cmd, opts, cb) => {
    // Support both callback and promisify forms — promisify supplies a cb.
    if (typeof opts === 'function') { cb = opts; opts = undefined; }
    Promise.resolve()
      .then(() => execMock(cmd, opts))
      .then(({ stdout = '', stderr = '' } = {}) => cb?.(null, { stdout, stderr }))
      .catch(err => cb?.(err));
  },
}));

const { DependencyResolverTool } = await import('../dependencyResolverTool.js');

let tmpDir;

beforeAll(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'loxia-depres-test-'));
});

afterAll(async () => {
  try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* */ }
});

beforeEach(() => {
  execMock.mockReset();
});

// ────────────────────── identity & description ────────────────────────

describe('DependencyResolverTool — identity', () => {
  test('constructor sets the documented id', () => {
    expect(new DependencyResolverTool().id).toBe('dependency-resolver');
  });

  test('getDescription mentions modes and parameters', () => {
    const d = new DependencyResolverTool().getDescription();
    expect(d).toMatch(/check/);
    expect(d).toMatch(/\bfix\b/);
    expect(d).toMatch(/\bauto\b/);
    expect(d).toMatch(/includeDev/);
  });
});

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

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

  test('object input + defaults', () => {
    expect(tool.parseParameters({})).toEqual({ path: '.', mode: 'check', includeDev: true });
  });

  test('object input with overrides', () => {
    expect(tool.parseParameters({ path: './x', mode: 'fix', includeDev: false }))
      .toEqual({ path: './x', mode: 'fix', includeDev: false });
  });

  test('modern XML with both attrs and inner content (content wins)', () => {
    const xml = `<dependency-resolve path="attr-path" mode="check">
      <path>./inner</path><mode>fix</mode><include-dev>false</include-dev>
    </dependency-resolve>`;
    expect(tool.parseParameters(xml))
      .toEqual({ path: './inner', mode: 'fix', includeDev: false });
  });

  test('modern XML attrs-only', () => {
    const xml = '<dependency-resolve path="./src" mode="auto" include-dev="true"></dependency-resolve>';
    expect(tool.parseParameters(xml))
      .toEqual({ path: './src', mode: 'auto', includeDev: true });
  });

  test('modern XML empty body → defaults', () => {
    expect(tool.parseParameters('<dependency-resolve></dependency-resolve>'))
      .toEqual({ path: '.', mode: 'check', includeDev: true });
  });

  test('legacy [resolve] bracket format', () => {
    expect(tool.parseParameters('[resolve path="./lib" mode="fix" include-dev="false"]'))
      .toEqual({ path: './lib', mode: 'fix', includeDev: false });
  });

  test('throws on unrecognised string', () => {
    expect(() => tool.parseParameters('garbage')).toThrow(/Invalid dependency-resolve format/);
  });

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

// ───────────────────────── _parseBoolean ──────────────────────────────

describe('DependencyResolverTool — _parseBoolean', () => {
  const tool = new DependencyResolverTool();
  test.each([
    [true, true],
    [false, false],
    ['true', true],
    ['TRUE', true],
    ['1', true],
    ['false', false],
    ['0', false],
    ['anything-else', false],
    [undefined, true],          // uses defaultValue=true here
    [null, true],
    [42, true],                 // numbers fall through to defaultValue
  ])('_parseBoolean(%p) → %p', (input, expected) => {
    expect(tool._parseBoolean(input, true)).toBe(expected);
  });

  test('respects default when value missing', () => {
    expect(tool._parseBoolean(undefined, false)).toBe(false);
  });
});

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

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

  test('accepts fully-populated valid params', () => {
    expect(() => tool._validateParameters({ path: '.', mode: 'check', includeDev: true })).not.toThrow();
  });

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

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

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

  test('rejects non-boolean includeDev', () => {
    expect(() => tool._validateParameters({ includeDev: 'yes' })).toThrow(/includeDev must be a boolean/);
  });
});

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

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

  test('uses projectDir as working dir', () => {
    const r = tool._resolveAndValidatePath('./sub', { projectDir: '/tmp/p' });
    expect(r.replace(/\\/g, '/')).toMatch(/^\/tmp\/p\/sub$/i);
  });

  test('directoryAccess.workingDirectory overrides projectDir', () => {
    const r = tool._resolveAndValidatePath('./x', {
      projectDir: '/ignored',
      directoryAccess: { workingDirectory: '/real/wd' },
    });
    expect(r.replace(/\\/g, '/')).toMatch(/^\/real\/wd\/x$/i);
  });

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

// ────────────────────── semver: _parseVersion ────────────────────────

describe('DependencyResolverTool — _parseVersion', () => {
  const tool = new DependencyResolverTool();

  test('parses plain X.Y.Z', () => {
    expect(tool._parseVersion('4.17.21')).toEqual({
      major: 4, minor: 17, patch: 21, prerelease: null, build: null,
    });
  });

  test('parses pre-release', () => {
    expect(tool._parseVersion('1.0.0-alpha.1')).toMatchObject({
      major: 1, minor: 0, patch: 0, prerelease: ['alpha', '1'],
    });
  });

  test('parses build metadata', () => {
    expect(tool._parseVersion('1.0.0+sha.abcdef')).toMatchObject({
      major: 1, minor: 0, patch: 0, build: 'sha.abcdef',
    });
  });

  test('falls back to simple X.Y.Z extraction for non-strict input', () => {
    expect(tool._parseVersion('4.17.21-foo bar')).toMatchObject({ major: 4, minor: 17, patch: 21 });
  });

  test('returns null for garbage', () => {
    expect(tool._parseVersion('not-a-version')).toBeNull();
  });
});

// ───────────────────── semver: _compareVersions ──────────────────────

describe('DependencyResolverTool — _compareVersions + _comparePrerelease', () => {
  const tool = new DependencyResolverTool();
  const v = (s) => tool._parseVersion(s);

  test('major / minor / patch ordering', () => {
    expect(tool._compareVersions(v('2.0.0'), v('1.0.0'))).toBeGreaterThan(0);
    expect(tool._compareVersions(v('1.2.0'), v('1.1.0'))).toBeGreaterThan(0);
    expect(tool._compareVersions(v('1.1.2'), v('1.1.1'))).toBeGreaterThan(0);
    expect(tool._compareVersions(v('1.0.0'), v('1.0.0'))).toBe(0);
  });

  test('release > prerelease at same X.Y.Z', () => {
    expect(tool._compareVersions(v('1.0.0'), v('1.0.0-alpha'))).toBeGreaterThan(0);
    expect(tool._compareVersions(v('1.0.0-alpha'), v('1.0.0'))).toBeLessThan(0);
  });

  test('prerelease lex ordering', () => {
    expect(tool._compareVersions(v('1.0.0-alpha'), v('1.0.0-beta'))).toBeLessThan(0);
  });

  test('prerelease numeric vs alphanumeric (numeric < alphanumeric)', () => {
    expect(tool._compareVersions(v('1.0.0-1'), v('1.0.0-alpha'))).toBeLessThan(0);
    expect(tool._compareVersions(v('1.0.0-alpha'), v('1.0.0-1'))).toBeGreaterThan(0);
  });

  test('prerelease numeric vs numeric', () => {
    expect(tool._compareVersions(v('1.0.0-2'), v('1.0.0-10'))).toBeLessThan(0);
  });

  test('shorter prerelease has lower precedence when prefixes match', () => {
    expect(tool._compareVersions(v('1.0.0-alpha'), v('1.0.0-alpha.1'))).toBeLessThan(0);
  });
});

// ───────────────────── semver: _parseVersionRange ────────────────────

describe('DependencyResolverTool — _parseVersionRange', () => {
  const tool = new DependencyResolverTool();

  test.each([
    ['^4.17.0',   { operator: '^', major: 4, minor: 17, patch: 0 }],
    ['~4.17.0',   { operator: '~', major: 4, minor: 17, patch: 0 }],
    ['>=4.0.0',   { operator: '>=', major: 4, minor: 0, patch: 0 }],
    ['<5.0.0',    { operator: '<', major: 5, minor: 0, patch: 0 }],
    ['<=2.1.1',   { operator: '<=', major: 2, minor: 1, patch: 1 }],
    ['>1.0.0',    { operator: '>', major: 1, minor: 0, patch: 0 }],
    ['4.17.21',   { operator: '=', major: 4, minor: 17, patch: 21 }],
  ])('parses %s', (input, expected) => {
    expect(tool._parseVersionRange(input)).toMatchObject(expected);
  });

  test.each([
    ['4.x',   { operator: 'x-range', major: 4, minor: null, patch: null }],
    ['4.*',   { operator: 'x-range', major: 4, minor: null, patch: null }],
    ['4.17.x',{ operator: 'x-range', major: 4, minor: 17, patch: null }],
    ['*',     { operator: 'x-range', major: null, minor: null, patch: null }],
    ['x',     { operator: 'x-range', major: null, minor: null, patch: null }],
  ])('parses X-range %s', (input, expected) => {
    expect(tool._parseVersionRange(input)).toMatchObject(expected);
  });

  test('returns null for unparseable input', () => {
    expect(tool._parseVersionRange('not-a-range')).toBeNull();
  });
});

// ───────────────── semver: _isUpdateAvailable variants ───────────────

describe('DependencyResolverTool — _isUpdateAvailable variants', () => {
  const tool = new DependencyResolverTool();
  const r = (s) => tool._parseVersionRange(s);
  const v = (s) => tool._parseVersion(s);

  test('caret: ^4.17.0 allows 4.18.5, blocks 5.0.0', () => {
    expect(tool._isUpdateAvailable(r('^4.17.0'), v('4.18.5'))).toBe(true);
    expect(tool._isUpdateAvailable(r('^4.17.0'), v('5.0.0'))).toBe(false);
  });

  test('caret: ^0.2.0 allows 0.2.5 but blocks 0.3.0', () => {
    expect(tool._isUpdateAvailable(r('^0.2.0'), v('0.2.5'))).toBe(true);
    expect(tool._isUpdateAvailable(r('^0.2.0'), v('0.3.0'))).toBe(false);
  });

  test('caret: ^0.0.3 allows 0.0.5 but blocks 0.1.0', () => {
    expect(tool._isUpdateAvailable(r('^0.0.3'), v('0.0.5'))).toBe(true);
    expect(tool._isUpdateAvailable(r('^0.0.3'), v('0.1.0'))).toBe(false);
  });

  test('tilde: ~4.17.0 allows 4.17.5 but blocks 4.18.0', () => {
    expect(tool._isUpdateAvailable(r('~4.17.0'), v('4.17.5'))).toBe(true);
    expect(tool._isUpdateAvailable(r('~4.17.0'), v('4.18.0'))).toBe(false);
  });

  test('>= and >: simple greater-than comparisons', () => {
    expect(tool._isUpdateAvailable(r('>=1.0.0'), v('2.0.0'))).toBe(true);
    expect(tool._isUpdateAvailable(r('>1.0.0'), v('1.0.0'))).toBe(false);
  });

  test('< and <=', () => {
    expect(tool._isUpdateAvailable(r('<2.0.0'), v('1.9.0'))).toBe(true);
    expect(tool._isUpdateAvailable(r('<2.0.0'), v('2.0.0'))).toBe(false);
    expect(tool._isUpdateAvailable(r('<=2.0.0'), v('2.0.0'))).toBe(true);
  });

  test('X-range: 4.x matches 4.99.99, blocks 5.0.0', () => {
    expect(tool._isUpdateAvailable(r('4.x'), v('4.99.99'))).toBe(true);
    expect(tool._isUpdateAvailable(r('4.x'), v('5.0.0'))).toBe(false);
  });

  test('X-range: 4.17.x', () => {
    expect(tool._isUpdateAvailable(r('4.17.x'), v('4.17.99'))).toBe(true);
    expect(tool._isUpdateAvailable(r('4.17.x'), v('4.18.0'))).toBe(false);
  });

  test('X-range: * matches anything', () => {
    expect(tool._isUpdateAvailable(r('*'), v('99.99.99'))).toBe(true);
  });

  test('exact: 4.17.21 → update available when latest is newer', () => {
    expect(tool._isUpdateAvailable(r('4.17.21'), v('4.17.22'))).toBe(true);
    expect(tool._isUpdateAvailable(r('4.17.21'), v('4.17.21'))).toBe(false);
  });
});

// ──────────────────── semver: complex ranges ─────────────────────────

describe('DependencyResolverTool — complex ranges', () => {
  const tool = new DependencyResolverTool();

  test('_isComplexRange flags OR and AND ranges', () => {
    expect(tool._isComplexRange('^4.0.0 || ^5.0.0')).toBe(true);
    expect(tool._isComplexRange('>=4.0.0 <5.0.0')).toBe(true);
    expect(tool._isComplexRange('^4.0.0')).toBe(false);
    expect(tool._isComplexRange('~4.0.0')).toBe(false);
    expect(tool._isComplexRange('4.0.0')).toBe(false);
  });

  test('_parseComplexRange: OR splits into branches', () => {
    const parsed = tool._parseComplexRange('^4.0.0 || ^5.0.0');
    expect(parsed.type).toBe('or');
    expect(parsed.ranges).toHaveLength(2);
  });

  test('_parseComplexRange: AND splits into constraints', () => {
    const parsed = tool._parseComplexRange('>=4.0.0 <5.0.0');
    expect(parsed.type).toBe('and');
    expect(parsed.ranges).toHaveLength(2);
  });

  test('_satisfiesComplexRange: OR — any branch wins', () => {
    const parsed = tool._parseComplexRange('^4.0.0 || ^5.0.0');
    const v = tool._parseVersion('5.1.0');
    expect(tool._satisfiesComplexRange(parsed, v)).toBe(true);
  });

  test('_satisfiesComplexRange: AND — all must satisfy', () => {
    const parsed = tool._parseComplexRange('>=4.0.0 <5.0.0');
    expect(tool._satisfiesComplexRange(parsed, tool._parseVersion('4.5.0'))).toBe(true);
    expect(tool._satisfiesComplexRange(parsed, tool._parseVersion('5.0.0'))).toBe(false);
    expect(tool._satisfiesComplexRange(parsed, tool._parseVersion('3.0.0'))).toBe(false);
  });

  test('_satisfiesComplexRange: bad input returns false', () => {
    expect(tool._satisfiesComplexRange(null, tool._parseVersion('1.0.0'))).toBe(false);
    expect(tool._satisfiesComplexRange({ type: 'unknown' }, tool._parseVersion('1.0.0'))).toBe(false);
  });

  test('_satisfiesRange: null guards', () => {
    expect(tool._satisfiesRange(null, tool._parseVersion('1.0.0'))).toBe(false);
  });
});

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

describe('DependencyResolverTool — execute()', () => {
  let originalFetch;

  beforeEach(() => {
    originalFetch = global.fetch;
  });

  afterEach(() => {
    global.fetch = originalFetch;
  });

  function mockFetch(versionByPkg) {
    global.fetch = jest.fn(async (url) => {
      const m = url.match(/registry\.npmjs\.org\/([^?]+)/);
      const pkg = m ? decodeURIComponent(m[1]) : '';
      if (versionByPkg[pkg]) {
        return {
          ok: true,
          status: 200,
          json: async () => ({ 'dist-tags': { latest: versionByPkg[pkg] } }),
        };
      }
      return { ok: false, status: 404, statusText: 'Not Found' };
    });
  }

  async function makeProj(name, packageJson) {
    const dir = path.join(tmpDir, name);
    await fs.mkdir(dir, { recursive: true });
    await fs.writeFile(path.join(dir, 'package.json'), JSON.stringify(packageJson, null, 2));
    return dir;
  }

  test('error when package.json is missing', async () => {
    const dir = path.join(tmpDir, 'no-pkg');
    await fs.mkdir(dir, { recursive: true });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'check', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/No package.json/);
  });

  test('reports zero deps when package.json has none', async () => {
    const dir = await makeProj('empty-pkg', { name: 'x', version: '1.0.0' });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'check', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.totalDependencies).toBe(0);
  });

  test('check mode: reports available updates without modifying files', async () => {
    const dir = await makeProj('check-mode', {
      name: 'x', version: '1.0.0',
      dependencies: { 'lodash': '^4.17.0' },
      devDependencies: { 'jest': '^29.0.0' },
    });
    mockFetch({ 'lodash': '4.17.21', 'jest': '29.0.0' });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'check', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.totalDependencies).toBe(2);
    expect(r.statistics.updatesAvailable).toBeGreaterThanOrEqual(1);
    // package.json should be untouched.
    const pkg = JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf-8'));
    expect(pkg.dependencies.lodash).toBe('^4.17.0');
  });

  test('check mode skips devDependencies when includeDev=false', async () => {
    const dir = await makeProj('no-dev', {
      name: 'x', version: '1.0.0',
      dependencies: { 'lodash': '^4.17.0' },
      devDependencies: { 'jest': '^29.0.0' },
    });
    mockFetch({ 'lodash': '4.17.21', 'jest': '29.99.0' });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'check', includeDev: false },
      { projectDir: dir },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.totalDependencies).toBe(1); // only lodash
  });

  test('all dependencies up-to-date → no updates reported', async () => {
    const dir = await makeProj('current', {
      name: 'x', version: '1.0.0',
      dependencies: { 'lodash': '^4.17.21' },
    });
    mockFetch({ 'lodash': '4.17.21' });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'check', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.updatesAvailable).toBe(0);
    expect(r.output).toMatch(/up-to-date/);
  });

  test('fix mode applies updates + runs npm install', async () => {
    const dir = await makeProj('fix-mode', {
      name: 'x', version: '1.0.0',
      dependencies: { 'lodash': '^4.17.0' },
    });
    mockFetch({ 'lodash': '4.17.21' });
    execMock.mockResolvedValue({ stdout: 'added 1 package', stderr: '' });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'fix', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.updatesApplied).toBe(1);
    // package.json should reflect the new version
    const pkg = JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf-8'));
    expect(pkg.dependencies.lodash).toMatch(/4\.17\.21/);
    // backup created
    expect(r.backupCreated).toBe(true);
  });

  test('fix mode rolls back when npm install fails', async () => {
    const dir = await makeProj('install-fail', {
      name: 'x', version: '1.0.0',
      dependencies: { 'lodash': '^4.17.0' },
    });
    const originalPkg = await fs.readFile(path.join(dir, 'package.json'), 'utf-8');
    mockFetch({ 'lodash': '4.17.21' });
    execMock.mockRejectedValue(new Error('install crashed'));
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'fix', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/install crashed/);
    // package.json should be restored from backup.
    const finalPkg = await fs.readFile(path.join(dir, 'package.json'), 'utf-8');
    expect(finalPkg).toBe(originalPkg);
  });

  test('fix mode without updates returns clean no-op', async () => {
    const dir = await makeProj('fix-noop', {
      name: 'x', version: '1.0.0',
      dependencies: { 'lodash': '^4.17.21' },
    });
    mockFetch({ 'lodash': '4.17.21' });
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'fix', includeDev: true },
      { projectDir: dir },
    );
    expect(r.success).toBe(true);
    expect(r.statistics.updatesApplied).toBe(0);
    expect(r.message).toMatch(/up-to-date/);
    // npm install should NOT have been called.
    expect(execMock).not.toHaveBeenCalled();
  });

  test('returns structured error on invalid mode (validation)', async () => {
    const r = await new DependencyResolverTool().execute(
      { path: '.', mode: 'turbo' },
      { projectDir: tmpDir },
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Invalid mode/);
  });
});
