/**
 * cloneDetectionTool.test.js — coverage tests for the clone-detection tool.
 *
 * Strategy:
 *   - The tool delegates to `analyzers/codeCloneDetector/index.js` which
 *     wires together a real Babel parser, file scanner, etc. Running the
 *     real pipeline in unit tests is heavy and brittle (depends on actual
 *     source files). The tool's value as a wrapper is in:
 *       * parameter parsing from XML tags,
 *       * input validation,
 *       * formatting raw detector output into summary/recommendations/
 *         detailed shapes,
 *       * directory-access enforcement and error wrapping.
 *     We test exactly those by injecting a stub detector via the lazy-
 *     load hook the tool already exposes (`tool.cloneDetector = stub`).
 *   - Coverage of the analyzer/detector internals belongs in their own
 *     test files.
 */

import { describe, test, expect, jest } from '@jest/globals';
import CloneDetectionTool from '../cloneDetectionTool.js';

// Minimal stub of the orchestrator the tool lazy-loads. The tool only
// calls `detector.run(fullDir, null)` — anything else is internal.
function makeStubDetector(report) {
  return {
    run: jest.fn(async () => report),
  };
}

// Realistic-shaped clone report; mirrors what RefactoringAnalyzer emits.
function makeReport({ priorityFilter, count = 3 } = {}) {
  const clones = Array.from({ length: count }, (_, i) => ({
    id: `clone-${i + 1}`,
    type: 'Type1',
    confidence: 0.95,
    instances: [
      { file: 'src/a.js', startLine: 1, endLine: 10, code: Array(15).fill('line').join('\n') },
      { file: 'src/b.js', startLine: 20, endLine: 30, code: 'short' },
    ],
    metrics: {
      instanceCount: 2,
      lineCount: 10,
      filesCovered: 2,
      impactScore: 80 - i * 10,
    },
    refactoringAdvice: {
      priority: priorityFilter || ['high', 'medium', 'low'][i % 3],
      strategy: 'extract-function',
      suggestedName: `extracted${i}`,
      reasoning: 'Reduces duplication',
      estimatedEffort: 'low',
      benefits: ['fewer-bugs', 'easier-maintenance'],
      actionableSteps: ['Step 1', 'Step 2'],
    },
  }));

  return {
    clones,
    summary: {
      totalFiles: 10,
      totalClones: clones.length,
      totalDuplicatedLines: 50,
      duplicationPercentage: 5.2,
      priorityCounts: { high: 1, medium: 1, low: 1 },
    },
  };
}

// Build a minimal context the tool will accept without trying to use the
// real DirectoryAccessManager filesystem checks.
function makeContext({ projectDir = '/tmp/project' } = {}) {
  const directoryAccess = {
    workingDirectory: projectDir,
    writeEnabledDirectories: [],
    readOnlyDirectories: [projectDir],
    restrictToProject: true,
  };
  return { projectDir, agentId: 'test-agent', directoryAccess };
}

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

describe('CloneDetectionTool — identity & schema', () => {
  test('constructor uses sensible defaults', () => {
    const tool = new CloneDetectionTool();
    expect(tool.requiresProject).toBe(true);
    expect(tool.isAsync).toBe(true);
    expect(tool.timeout).toBe(300000);
    expect(tool.defaultMinTokens).toBe(50);
    expect(tool.defaultMinLines).toBe(5);
    expect(tool.defaultSimilarityThreshold).toBe(0.85);
    expect(tool.directoryAccessManager).toBeDefined();
  });

  test('constructor honours overrides', () => {
    const tool = new CloneDetectionTool({
      timeout: 60000,
      defaultMinTokens: 25,
      defaultMinLines: 10,
      defaultSimilarityThreshold: 0.7,
      maxFileSize: 1000,
      maxConcurrentOperations: 4,
    });
    expect(tool.timeout).toBe(60000);
    expect(tool.defaultMinTokens).toBe(25);
    expect(tool.defaultMinLines).toBe(10);
    expect(tool.defaultSimilarityThreshold).toBe(0.7);
    expect(tool.maxFileSize).toBe(1000);
    expect(tool.maxConcurrentOperations).toBe(4);
  });

  test('getSupportedActions returns the one documented action', () => {
    expect(new CloneDetectionTool().getSupportedActions()).toEqual(['detect-clones']);
  });

  test('getRequiredParameters returns ["actions"]', () => {
    expect(new CloneDetectionTool().getRequiredParameters()).toEqual(['actions']);
  });

  test('getDescription mentions the action and key parameters', () => {
    const d = new CloneDetectionTool().getDescription();
    expect(d).toMatch(/detect-clones/);
    expect(d).toMatch(/similarityThreshold/);
    expect(d).toMatch(/outputMode/);
    expect(d).toMatch(/priorityFilter/);
  });

  test('getParameterSchema mirrors documented constraints', () => {
    const schema = new CloneDetectionTool().getParameterSchema();
    expect(schema.required).toContain('actions');
    const itemProps = schema.properties.actions.items.properties;
    expect(itemProps.minTokens).toMatchObject({ minimum: 10, maximum: 1000 });
    expect(itemProps.similarityThreshold).toMatchObject({ minimum: 0.5, maximum: 1.0 });
    expect(itemProps.outputMode.enum).toEqual(['summary', 'detailed', 'recommendations']);
  });
});

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

describe('CloneDetectionTool — parseParameters', () => {
  test('parses a self-closing detect-clones tag with all kebab-case attrs', () => {
    const tool = new CloneDetectionTool();
    const out = tool.parseParameters(
      '<detect-clones directory="src" min-tokens="30" min-lines="3" ' +
      'similarity-threshold="0.9" priority-filter="high" max-results="5" ' +
      'output-mode="summary"/>',
    );
    expect(out.actions).toHaveLength(1);
    expect(out.actions[0]).toMatchObject({
      type: 'detect-clones',
      directory: 'src',
      minTokens: 30,
      minLines: 3,
      similarityThreshold: 0.9,
      priorityFilter: 'high',
      maxResults: 5,
      outputMode: 'summary',
    });
    // Kebab-case keys should NOT survive the rename.
    expect(out.actions[0]).not.toHaveProperty('min-tokens');
    expect(out.actions[0]).not.toHaveProperty('similarity-threshold');
  });

  test('returns an empty actions array when no tag is present', () => {
    const tool = new CloneDetectionTool();
    const out = tool.parseParameters('no tags here at all');
    expect(out.actions).toEqual([]);
    expect(out.rawContent).toBe('no tags here at all');
  });

  test('parses multiple detect-clones tags', () => {
    const tool = new CloneDetectionTool();
    const out = tool.parseParameters(
      '<detect-clones directory="src"/><detect-clones directory="lib" max-results="3"/>',
    );
    expect(out.actions).toHaveLength(2);
    expect(out.actions[0].directory).toBe('src');
    expect(out.actions[1]).toMatchObject({ directory: 'lib', maxResults: 3 });
  });

  test('wraps regex/parser failures with a friendly error', () => {
    const tool = new CloneDetectionTool();
    // Force a throw by passing a non-string (regex.exec will fail when
    // .length is read inside the parser flow).
    expect(() => tool.parseParameters(null)).toThrow(/Failed to parse clone detection parameters/);
  });
});

// ──────────────────── customValidateParameters ────────────────────────

describe('CloneDetectionTool — customValidateParameters', () => {
  const tool = new CloneDetectionTool();

  test('requires a non-empty actions array', () => {
    expect(tool.customValidateParameters({}).valid).toBe(false);
    expect(tool.customValidateParameters({ actions: [] }).valid).toBe(false);
    expect(tool.customValidateParameters({ actions: 'nope' }).valid).toBe(false);
  });

  test('requires a type on every action', () => {
    const r = tool.customValidateParameters({ actions: [{ directory: 'src' }] });
    expect(r.valid).toBe(false);
    expect(r.errors[0]).toMatch(/type is required/);
  });

  test('rejects unknown action types', () => {
    const r = tool.customValidateParameters({ actions: [{ type: 'frobnicate' }] });
    expect(r.valid).toBe(false);
    expect(r.errors[0]).toMatch(/unknown action type/i);
  });

  test('requires directory on detect-clones', () => {
    const r = tool.customValidateParameters({ actions: [{ type: 'detect-clones' }] });
    expect(r.valid).toBe(false);
    expect(r.errors[0]).toMatch(/directory is required/);
  });

  test('rejects out-of-range minTokens', () => {
    const r1 = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', minTokens: 5 }] });
    const r2 = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', minTokens: 2000 }] });
    expect(r1.valid).toBe(false);
    expect(r2.valid).toBe(false);
    expect(r1.errors[0]).toMatch(/min-tokens/);
  });

  test('rejects out-of-range minLines', () => {
    const r = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', minLines: 0 }] });
    expect(r.valid).toBe(false);
    expect(r.errors[0]).toMatch(/min-lines/);
  });

  test('rejects out-of-range similarityThreshold', () => {
    const r1 = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', similarityThreshold: 0.4 }] });
    const r2 = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', similarityThreshold: 1.5 }] });
    expect(r1.valid).toBe(false);
    expect(r2.valid).toBe(false);
  });

  test('rejects unknown priorityFilter', () => {
    const r = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', priorityFilter: 'urgent' }] });
    expect(r.valid).toBe(false);
    expect(r.errors[0]).toMatch(/priority-filter/);
  });

  test('rejects unknown outputMode', () => {
    const r = tool.customValidateParameters({ actions: [{ type: 'detect-clones', directory: 's', outputMode: 'xml' }] });
    expect(r.valid).toBe(false);
    expect(r.errors[0]).toMatch(/output-mode/);
  });

  test('passes when everything is in range', () => {
    const r = tool.customValidateParameters({
      actions: [{
        type: 'detect-clones', directory: 'src',
        minTokens: 50, minLines: 5, similarityThreshold: 0.85,
        priorityFilter: 'high', outputMode: 'detailed',
      }],
    });
    expect(r.valid).toBe(true);
    expect(r.errors).toEqual([]);
  });
});

// ───────────────────────── format helpers ─────────────────────────────

describe('CloneDetectionTool — output formatters', () => {
  const tool = new CloneDetectionTool();
  // createRelativePath spreads readOnlyDirectories + writeEnabledDirectories
  // and reads workingDirectory; all three must be present.
  const accessConfig = {
    workingDirectory: '/proj',
    readOnlyDirectories: ['/proj'],
    writeEnabledDirectories: [],
  };
  const fullDir = '/proj/src';

  test('formatSummaryOutput keeps only top-5 clones', () => {
    const report = makeReport({ count: 8 });
    const out = tool.formatSummaryOutput(report, report.clones, fullDir, accessConfig);
    expect(out.outputMode).toBe('summary');
    expect(out.summary.topClones).toHaveLength(5);
    expect(out.summary.topClones[0]).toHaveProperty('id');
    expect(out.summary.totalClones).toBe(8);
  });

  test('formatRecommendationsOutput emits per-clone refactoring blocks', () => {
    const report = makeReport({ count: 2 });
    const out = tool.formatRecommendationsOutput(report, report.clones, fullDir, accessConfig);
    expect(out.outputMode).toBe('recommendations');
    expect(out.recommendations).toHaveLength(2);
    expect(out.recommendations[0]).toMatchObject({
      priority: expect.any(String),
      strategy: 'extract-function',
      steps: expect.any(Array),
      metrics: { instances: 2, lines: 10 },
    });
  });

  test('formatDetailedOutput truncates long code snippets to 10 lines + marker', () => {
    const report = makeReport({ count: 1 });
    const out = tool.formatDetailedOutput(report, report.clones, fullDir, accessConfig);
    expect(out.outputMode).toBe('detailed');
    const long = out.clones[0].instances[0];
    expect(long.code.split('\n').length).toBeLessThanOrEqual(11);
    expect(long.code).toMatch(/truncated/);
    // Short snippets stay intact (no marker)
    const short = out.clones[0].instances[1];
    expect(short.code).toBe('short');
  });
});

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

describe('CloneDetectionTool — execute (with stubbed detector)', () => {
  test('routes to detect-clones and returns detailed output by default', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = makeStubDetector(makeReport({ count: 2 })); // skip lazy import
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: 'src' }] },
      makeContext(),
    );
    expect(r.success).toBe(true);
    expect(r.results).toHaveLength(1);
    expect(r.results[0].outputMode).toBe('detailed');
    expect(r.results[0].clones).toHaveLength(2);
    expect(tool.cloneDetector.run).toHaveBeenCalledTimes(1);
  });

  test('routes to summary output when outputMode=summary', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = makeStubDetector(makeReport({ count: 3 }));
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: 'src', outputMode: 'summary' }] },
      makeContext(),
    );
    expect(r.results[0].outputMode).toBe('summary');
  });

  test('routes to recommendations output when outputMode=recommendations', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = makeStubDetector(makeReport({ count: 2 }));
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: 'src', outputMode: 'recommendations' }] },
      makeContext(),
    );
    expect(r.results[0].outputMode).toBe('recommendations');
  });

  test('applies priorityFilter before maxResults slicing', async () => {
    const tool = new CloneDetectionTool();
    // Report with mixed priorities; keep only "high".
    const report = makeReport({ count: 6 }); // priorities cycle high/medium/low
    tool.cloneDetector = makeStubDetector(report);
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: 'src', priorityFilter: 'high', maxResults: 1 }] },
      makeContext(),
    );
    expect(r.success).toBe(true);
    expect(r.results[0].clones).toHaveLength(1);
    expect(r.results[0].clones[0].refactoringAdvice.priority).toBe('high');
  });

  test('returns the "no files / no clones" shape when detector yields null', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = makeStubDetector(null);
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: 'src' }] },
      makeContext(),
    );
    expect(r.results[0].success).toBe(true);
    expect(r.results[0].summary.totalClones).toBe(0);
    expect(r.results[0].message).toMatch(/No files found/);
  });

  test('per-action errors are captured without failing the overall execute', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = makeStubDetector(makeReport());
    // First action ok, second action with bogus type — should be reported
    // as a per-action error (the dispatcher throws unknown action types).
    const r = await tool.execute(
      { actions: [
        { type: 'detect-clones', directory: 'src' },
        { type: 'wat' },
      ] },
      makeContext(),
    );
    expect(r.success).toBe(true); // overall still "success: true"
    expect(r.results).toHaveLength(2);
    expect(r.results[0].success).not.toBe(false);
    expect(r.results[1]).toMatchObject({ success: false, error: expect.stringMatching(/Unknown action type/) });
  });

  test('per-action detector failures are captured', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = { run: jest.fn(async () => { throw new Error('parser crashed'); }) };
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: 'src' }] },
      makeContext(),
    );
    expect(r.success).toBe(true);
    expect(r.results[0]).toMatchObject({ success: false, error: expect.stringMatching(/parser crashed/) });
  });

  test('refuses when DirectoryAccessManager denies read access', async () => {
    const tool = new CloneDetectionTool();
    tool.cloneDetector = makeStubDetector(makeReport());
    // Hand-craft an access config that won't permit /etc.
    const r = await tool.execute(
      { actions: [{ type: 'detect-clones', directory: '/etc/passwd-fake-dir' }] },
      makeContext({ projectDir: '/tmp/project' }),
    );
    expect(r.success).toBe(true);
    expect(r.results[0]).toMatchObject({ success: false, error: expect.stringMatching(/Read access denied|denied/) });
  });
});

// ──────────────────────── getCloneDetector ────────────────────────────

describe('CloneDetectionTool — getCloneDetector', () => {
  test('lazily loads the detector and caches it across calls', async () => {
    const tool = new CloneDetectionTool();
    expect(tool.cloneDetector).toBeNull();
    const d1 = await tool.getCloneDetector();
    expect(d1).toBeTruthy();
    expect(tool.cloneDetector).toBe(d1);
    const d2 = await tool.getCloneDetector();
    expect(d2).toBe(d1); // cached
  });
});
