// Feature: atlas-metrics-report, Property 1: Metrics aggregation correctness

import fc from 'fast-check';

// ---------------------------------------------------------------------------
// Polyfill TextDecoder/TextEncoder for jsdom environment (Express 5 dependency)
// ---------------------------------------------------------------------------
if (typeof globalThis.TextDecoder === 'undefined') {
  const { TextDecoder, TextEncoder } = require('util');
  globalThis.TextDecoder = TextDecoder;
  globalThis.TextEncoder = TextEncoder;
}

// ---------------------------------------------------------------------------
// Mock backend dependencies so we can import the pure function
// without pulling in Express, SQLite, etc.
// ---------------------------------------------------------------------------
jest.mock('express', () => ({ Router: jest.fn(() => ({ get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn() })) }));
jest.mock('pg', () => ({ Pool: jest.fn(() => ({ query: jest.fn() })) }));
jest.mock('../../../../../backend/db', () => ({ query: jest.fn(() => Promise.resolve({ rows: [] })) }), { virtual: true });
jest.mock('../../../../../backend/middleware/auth', () => ({ requireAuth: jest.fn(() => (req, res, next) => next()), requireGroup: jest.fn(() => (req, res, next) => next()) }), { virtual: true });
jest.mock('../../../../../backend/helpers/auditLog', () => jest.fn(), { virtual: true });
jest.mock('../../../../../backend/helpers/atlasApi', () => ({
  isConfigured: false,
  atlasGet: jest.fn(),
  atlasPut: jest.fn(),
  atlasPatch: jest.fn(),
  atlasPost: jest.fn(),
}), { virtual: true });

// Now import the pure function
const { aggregateAtlasMetrics } = require('../../../../../backend/routes/atlas');

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

const PLAN_TYPES = ['decommission', 'remediation', 'false_positive', 'risk_acceptance', 'scan_exclusion'];
const PLAN_STATUSES = ['active', 'expired', 'completed'];

/** Generate a single plan object with plan_type and status */
const planArb = fc.record({
  plan_type: fc.constantFrom(...PLAN_TYPES),
  status: fc.constantFrom(...PLAN_STATUSES),
});

/** Generate a valid plans_json string (JSON array of plan objects) */
const validPlansJsonArb = fc
  .array(planArb, { minLength: 0, maxLength: 10 })
  .map((plans) => JSON.stringify(plans));

/** Generate an invalid JSON string that will fail JSON.parse */
const invalidPlansJsonArb = fc.constantFrom(
  '{bad json',
  'not json at all',
  '{{[',
  '',
  'undefined',
);

/** Generate a plans_json value — either valid JSON or invalid */
const plansJsonArb = fc.oneof(
  { weight: 3, arbitrary: validPlansJsonArb },
  { weight: 1, arbitrary: invalidPlansJsonArb },
);

/** Generate a single cache row */
const cacheRowArb = fc.record({
  has_action_plan: fc.constantFrom(0, 1),
  plans_json: plansJsonArb,
});

// ---------------------------------------------------------------------------
// Helper: manually compute expected metrics for comparison
// ---------------------------------------------------------------------------
function computeExpected(rows) {
  const expected = {
    totalHosts: rows.length,
    hostsWithPlans: 0,
    hostsWithoutPlans: 0,
    plansByType: {},
    plansByStatus: {},
    totalPlans: 0,
  };

  for (const row of rows) {
    if (row.has_action_plan === 1) {
      expected.hostsWithPlans++;
    } else {
      expected.hostsWithoutPlans++;
    }

    let plans;
    try {
      plans = JSON.parse(row.plans_json);
    } catch (e) {
      continue;
    }

    if (!Array.isArray(plans)) continue;

    for (const plan of plans) {
      expected.totalPlans++;
      if (plan.plan_type) {
        expected.plansByType[plan.plan_type] = (expected.plansByType[plan.plan_type] || 0) + 1;
      }
      if (plan.status) {
        expected.plansByStatus[plan.status] = (expected.plansByStatus[plan.status] || 0) + 1;
      }
    }
  }

  return expected;
}

// ---------------------------------------------------------------------------
// Property 1: Metrics aggregation correctness
// Validates: Requirements 1.3, 1.4, 1.5
// ---------------------------------------------------------------------------

describe('Property 1: Metrics aggregation correctness', () => {
  test('totalHosts equals rows.length', () => {
    fc.assert(
      fc.property(
        fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
        (rows) => {
          const result = aggregateAtlasMetrics(rows);
          expect(result.totalHosts).toBe(rows.length);
        },
      ),
      { numRuns: 100 },
    );
  });

  test('hostsWithPlans + hostsWithoutPlans equals totalHosts', () => {
    fc.assert(
      fc.property(
        fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
        (rows) => {
          const result = aggregateAtlasMetrics(rows);
          expect(result.hostsWithPlans + result.hostsWithoutPlans).toBe(result.totalHosts);
        },
      ),
      { numRuns: 100 },
    );
  });

  test('hostsWithPlans equals count of rows where has_action_plan === 1', () => {
    fc.assert(
      fc.property(
        fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
        (rows) => {
          const result = aggregateAtlasMetrics(rows);
          const expectedWithPlans = rows.filter((r) => r.has_action_plan === 1).length;
          expect(result.hostsWithPlans).toBe(expectedWithPlans);
        },
      ),
      { numRuns: 100 },
    );
  });

  test('totalPlans equals sum of valid plan array lengths', () => {
    fc.assert(
      fc.property(
        fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
        (rows) => {
          const result = aggregateAtlasMetrics(rows);
          const expected = computeExpected(rows);
          expect(result.totalPlans).toBe(expected.totalPlans);
        },
      ),
      { numRuns: 100 },
    );
  });

  test('plansByType and plansByStatus counts match individual plan fields', () => {
    fc.assert(
      fc.property(
        fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
        (rows) => {
          const result = aggregateAtlasMetrics(rows);
          const expected = computeExpected(rows);
          expect(result.plansByType).toEqual(expected.plansByType);
          expect(result.plansByStatus).toEqual(expected.plansByStatus);
        },
      ),
      { numRuns: 100 },
    );
  });

  test('rows with invalid JSON are counted in host totals but excluded from plan counts', () => {
    fc.assert(
      fc.property(
        fc.array(
          fc.record({
            has_action_plan: fc.constantFrom(0, 1),
            plans_json: invalidPlansJsonArb,
          }),
          { minLength: 1, maxLength: 20 },
        ),
        (rows) => {
          const result = aggregateAtlasMetrics(rows);

          // Host totals should still be correct
          expect(result.totalHosts).toBe(rows.length);
          expect(result.hostsWithPlans + result.hostsWithoutPlans).toBe(rows.length);

          // No plans should be counted since all JSON is invalid
          expect(result.totalPlans).toBe(0);
          expect(result.plansByType).toEqual({});
          expect(result.plansByStatus).toEqual({});
        },
      ),
      { numRuns: 100 },
    );
  });
});
