import fc from 'fast-check';
import { computeWorstStatus, groupByMetricFamily } from '../CompliancePage';

// ---------------------------------------------------------------------------
// Generators
// ---------------------------------------------------------------------------

const VALID_STATUSES = [
  'Below 15% of Target',
  'Within 15% of Target',
  'Meets/Exceeds Target',
];

const statusArb = fc.constantFrom(...VALID_STATUSES);

const summaryEntryArb = fc.record({
  metric_id: fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/),
  team: fc.constantFrom('STEAM', 'ACCESS-ENG'),
  priority: fc.constantFrom('High', 'Medium', 'Low'),
  non_compliant: fc.nat({ max: 500 }),
  compliant: fc.nat({ max: 500 }),
  total: fc.nat({ max: 1000 }),
  compliance_pct: fc.double({ min: 0, max: 1, noNaN: true }),
  target: fc.double({ min: 0, max: 1, noNaN: true }),
  status: statusArb,
  description: fc.string({ minLength: 1, maxLength: 50 }),
  category: fc.constantFrom(
    'Vulnerability Management',
    'Access & MFA',
    'Logging & Monitoring',
    'End-of-Life OS',
  ),
});

// ---------------------------------------------------------------------------
// Property 1: Grouping invariant — no entries lost or misplaced
// Validates: Requirements 1.1, 1.2
// ---------------------------------------------------------------------------

describe('Property 1: Grouping invariant — no entries lost or misplaced', () => {
  test('every entry appears in exactly one group, groups share metric_id, totals match', () => {
    fc.assert(
      fc.property(
        fc.array(summaryEntryArb, { minLength: 0, maxLength: 30 }),
        fc.constantFrom('STEAM', 'ACCESS-ENG'),
        (entries, team) => {
          const groups = groupByMetricFamily(entries, team);
          const teamEntries = entries.filter(
            (e) => e.team === team && e.metric_id,
          );

          // (c) total entries across groups equals team-filtered input count
          const totalGrouped = groups.reduce(
            (sum, g) => sum + g.entries.length,
            0,
          );
          expect(totalGrouped).toBe(teamEntries.length);

          // (b) all entries within a group share the same metric_id
          for (const group of groups) {
            for (const entry of group.entries) {
              expect(entry.metric_id).toBe(group.metricId);
            }
          }

          // (a) every team entry appears in exactly one group
          const allGroupedEntries = groups.flatMap((g) => g.entries);
          for (const entry of teamEntries) {
            const occurrences = allGroupedEntries.filter(
              (e) => e === entry,
            ).length;
            expect(occurrences).toBe(1);
          }
        },
      ),
      { numRuns: 100 },
    );
  });
});

// ---------------------------------------------------------------------------
// Property 2: Worst-status computation follows severity ordering
// Validates: Requirements 1.6, 3.1
// ---------------------------------------------------------------------------

const STATUS_SEVERITY = {
  'Below 15% of Target': 0,
  'Within 15% of Target': 1,
  'Meets/Exceeds Target': 2,
};

describe('Property 2: Worst-status computation follows severity ordering', () => {
  test('result is the status with the lowest severity rank present', () => {
    fc.assert(
      fc.property(
        fc.array(statusArb, { minLength: 1, maxLength: 20 }),
        (statuses) => {
          const result = computeWorstStatus(statuses);

          // Result must be a valid status
          expect(VALID_STATUSES).toContain(result);

          // Result must be the minimum severity present
          const minSeverity = Math.min(
            ...statuses.map((s) => STATUS_SEVERITY[s]),
          );
          expect(STATUS_SEVERITY[result]).toBe(minSeverity);

          // If array contains "Below 15% of Target", result must be that
          if (statuses.includes('Below 15% of Target')) {
            expect(result).toBe('Below 15% of Target');
          }
        },
      ),
      { numRuns: 100 },
    );
  });
});
