// dashboard.test.mjs — Tests for the cross-project dashboard aggregator
// Tests discovery, aggregation, caching, and grade calculation.
// Uses posture-scan fixtures as known projects.

import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { writeFile, mkdir, rm, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { resetCounter } from '../../scanners/lib/output.mjs';

// Import functions under test
import { discoverProjects, aggregate } from '../../scanners/dashboard-aggregator.mjs';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const FIXTURES = resolve(__dirname, '../fixtures/posture-scan');
const GRADE_A_FIXTURE = resolve(FIXTURES, 'grade-a-project');
const GRADE_F_FIXTURE = resolve(FIXTURES, 'grade-f-project');

// ---------------------------------------------------------------------------
// Discovery tests
// ---------------------------------------------------------------------------

describe('dashboard-aggregator: discoverProjects', () => {
  it('finds projects with CLAUDE.md marker', async () => {
    // The fixtures themselves have CLAUDE.md — use parent as search root with depth 2
    const projects = await discoverProjects({
      maxDepth: 2,
      extraPaths: [GRADE_A_FIXTURE],
    });
    assert.ok(projects.includes(GRADE_A_FIXTURE), 'Should find grade-a fixture via extraPaths');
  });

  it('finds projects with .claude-plugin marker', async () => {
    const projects = await discoverProjects({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
    });
    assert.ok(projects.includes(GRADE_A_FIXTURE));
  });

  it('deduplicates project paths', async () => {
    const projects = await discoverProjects({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE, GRADE_A_FIXTURE, GRADE_A_FIXTURE],
    });
    const count = projects.filter(p => p === GRADE_A_FIXTURE).length;
    assert.equal(count, 1, 'Should deduplicate');
  });

  it('returns sorted paths', async () => {
    const projects = await discoverProjects({
      maxDepth: 0,
      extraPaths: [GRADE_F_FIXTURE, GRADE_A_FIXTURE],
    });
    const filtered = projects.filter(p => p.includes('posture-scan'));
    for (let i = 1; i < filtered.length; i++) {
      assert.ok(filtered[i] >= filtered[i - 1], 'Should be sorted');
    }
  });

  it('handles non-existent extra paths gracefully', async () => {
    const projects = await discoverProjects({
      maxDepth: 0,
      extraPaths: ['/nonexistent/path/that/does/not/exist'],
    });
    assert.ok(Array.isArray(projects));
  });
});

// ---------------------------------------------------------------------------
// Aggregation tests
// ---------------------------------------------------------------------------

describe('dashboard-aggregator: aggregate', () => {
  let tmpCacheDir;

  beforeEach(async () => {
    resetCounter();
  });

  it('scans known fixtures and returns structured result', async () => {
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE, GRADE_F_FIXTURE],
      useCache: false,
    });

    // Meta
    assert.equal(result.meta.scanner, 'dashboard-aggregator');
    assert.ok(result.meta.version);
    assert.ok(result.meta.timestamp);
    assert.equal(result.meta.from_cache, false);

    // Machine
    assert.ok(result.machine.grade, 'Should have machine grade');
    assert.ok(result.machine.projects_scanned >= 2, `Expected >=2 projects, got ${result.machine.projects_scanned}`);

    // Projects array
    assert.ok(Array.isArray(result.projects));
    assert.ok(result.projects.length >= 2);

    // Each project has required fields
    for (const p of result.projects) {
      assert.ok(p.path, 'Project should have path');
      assert.ok(p.display_name, 'Project should have display_name');
      assert.ok(p.grade, 'Project should have grade');
      assert.ok(typeof p.risk_score === 'number', 'risk_score should be number');
      assert.ok(typeof p.findings_count === 'number', 'findings_count should be number');
      assert.ok(p.counts, 'Project should have counts');
    }
  });

  it('machine grade is weakest link', async () => {
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE, GRADE_F_FIXTURE],
      useCache: false,
    });

    // grade-f-project should drag machine grade down
    const fProject = result.projects.find(p => p.path === GRADE_F_FIXTURE);
    assert.ok(fProject, 'Should find grade-f fixture in results');
    assert.equal(fProject.grade, 'F', 'Grade F fixture should get F');

    // Machine grade should be F (weakest link)
    assert.equal(result.machine.grade, 'F', 'Machine grade should be F (weakest link)');
  });

  it('identifies worst category per project', async () => {
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_F_FIXTURE],
      useCache: false,
    });

    const fProject = result.projects.find(p => p.path === GRADE_F_FIXTURE);
    assert.ok(fProject);
    assert.ok(fProject.worst_category, 'Should identify worst category');
    assert.equal(fProject.worst_status, 'FAIL', 'Worst status should be FAIL for grade-f');
  });

  it('aggregates finding counts across projects', async () => {
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE, GRADE_F_FIXTURE],
      useCache: false,
    });

    assert.ok(typeof result.machine.total_findings === 'number');
    assert.ok(typeof result.machine.counts.critical === 'number');
    assert.ok(typeof result.machine.counts.high === 'number');

    // Total should match sum of per-project
    const sumFindings = result.projects.reduce((s, p) => s + p.findings_count, 0);
    assert.equal(result.machine.total_findings, sumFindings);
  });

  it('includes duration_ms per project', async () => {
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: false,
    });

    for (const p of result.projects) {
      assert.ok(typeof p.duration_ms === 'number', 'Should have duration_ms');
    }
  });

  it('records errors for invalid projects', async () => {
    // Create a fake project that will fail to scan properly
    const tmpDir = join(tmpdir(), `dashboard-test-err-${Date.now()}`);
    await mkdir(tmpDir, { recursive: true });
    await writeFile(join(tmpDir, 'CLAUDE.md'), '# Test\n');
    // Create a .claude dir with malformed settings.json to trigger issues
    // (posture-scanner should still succeed but with low grade)

    try {
      const result = await aggregate({
        maxDepth: 0,
        extraPaths: [tmpDir],
        useCache: false,
      });
      assert.ok(Array.isArray(result.errors));
      // The project should either be in projects or errors
      const found = result.projects.some(p => p.path === tmpDir) ||
                    result.errors.some(e => e.path === tmpDir);
      assert.ok(found, 'Tmp project should appear in results or errors');
    } finally {
      await rm(tmpDir, { recursive: true, force: true });
    }
  });
});

// ---------------------------------------------------------------------------
// Caching tests
// ---------------------------------------------------------------------------

describe('dashboard-aggregator: caching', () => {
  it('returns from_cache: true when cache is fresh', async () => {
    // First run: force fresh to populate cache
    const result1 = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: false,
    });
    assert.equal(result1.meta.from_cache, false);

    // Second run: should use cache (freshly written)
    const result2 = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: true,
      stalenessMs: 60000,
    });
    assert.equal(result2.meta.from_cache, true);
  });

  it('bypasses cache with useCache: false', async () => {
    // Populate cache
    await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: true,
      stalenessMs: 60000,
    });

    // Force fresh scan
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: false,
    });
    assert.equal(result.meta.from_cache, false);
  });

  it('rescans when cache is stale', async () => {
    // Populate cache
    await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: true,
    });

    // Use 0ms staleness threshold — everything is stale
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: true,
      stalenessMs: 0,
    });
    assert.equal(result.meta.from_cache, false);
  });
});

// ---------------------------------------------------------------------------
// Grade comparison tests
// ---------------------------------------------------------------------------

describe('dashboard-aggregator: grade logic', () => {
  it('grade-a only yields machine grade A', async () => {
    resetCounter();
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: false,
    });

    const aProject = result.projects.find(p => p.path === GRADE_A_FIXTURE);
    if (aProject && aProject.grade === 'A') {
      // If only Grade A projects, machine grade should be A
      const allA = result.projects.every(p => p.grade === 'A');
      if (allA) {
        assert.equal(result.machine.grade, 'A');
      }
    }
  });

  it('display_name uses tilde for home-relative paths', async () => {
    const result = await aggregate({
      maxDepth: 0,
      extraPaths: [GRADE_A_FIXTURE],
      useCache: false,
    });

    for (const p of result.projects) {
      if (p.path.startsWith(process.env.HOME || '/Users')) {
        assert.ok(p.display_name.startsWith('~/'), `Expected ~/... got ${p.display_name}`);
      }
    }
  });
});
