import fc from 'fast-check';
import MetricInfoPanel from '../MetricInfoPanel';

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

const DEFINITION_KEYS = [
  'metric_id',
  'metric_title',
  'asset_types',
  'asset_types_in_scope',
  'application_types_in_scope',
  'environment_in_scope',
  'status_in_scope',
  'instance_types_in_scope',
  'criticality_levels_in_scope',
  'exclusions',
  'special_conditions',
  'data_sources_required',
  'business_justification',
  'notes',
];

const metricDefinitionArb = fc.record({
  metric_id: fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/),
  metric_title: fc.string({ minLength: 1, maxLength: 80 }),
  asset_types: fc.string({ minLength: 0, maxLength: 60 }),
  asset_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
  application_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
  environment_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
  status_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
  instance_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
  criticality_levels_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
  exclusions: fc.string({ minLength: 0, maxLength: 60 }),
  special_conditions: fc.string({ minLength: 0, maxLength: 60 }),
  data_sources_required: fc.string({ minLength: 0, maxLength: 60 }),
  business_justification: fc.string({ minLength: 0, maxLength: 60 }),
  notes: fc.string({ minLength: 0, maxLength: 60 }),
});

/**
 * Generate an array of metric definitions with unique metric_id values.
 */
const uniqueDefinitionsArb = fc
  .array(metricDefinitionArb, { minLength: 1, maxLength: 20 })
  .map((defs) => {
    const seen = new Set();
    return defs.filter((d) => {
      if (seen.has(d.metric_id)) return false;
      seen.add(d.metric_id);
      return true;
    });
  })
  .filter((arr) => arr.length > 0);

// ---------------------------------------------------------------------------
// Property 4: Definition lookup returns correct entry or null
// Validates: Requirements 4.2, 4.6
// ---------------------------------------------------------------------------

describe('Property 4: Definition lookup returns correct entry or null', () => {
  test('lookup hits for IDs in the array and misses for IDs not in the array', () => {
    fc.assert(
      fc.property(
        uniqueDefinitionsArb,
        fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/),
        (definitions, queryId) => {
          // Build lookup map
          const lookup = {};
          for (const def of definitions) {
            lookup[def.metric_id] = def;
          }

          // Query with IDs from the array — expect hit
          for (const def of definitions) {
            expect(lookup[def.metric_id]).toBe(def);
          }

          // Query with a random ID — expect hit if present, miss if not
          const existsInArray = definitions.some(
            (d) => d.metric_id === queryId,
          );
          if (existsInArray) {
            expect(lookup[queryId]).toBeDefined();
          } else {
            expect(lookup[queryId]).toBeUndefined();
          }
        },
      ),
      { numRuns: 100 },
    );
  });
});

// ---------------------------------------------------------------------------
// Property 5: Detail panel renders all required definition fields
// Validates: Requirements 5.3
// ---------------------------------------------------------------------------

describe('Property 5: Detail panel renders all required definition fields', () => {
  test('RENDERED_FIELD_KEYS includes all required definition keys (excluding metric_id and metric_title)', () => {
    const renderedKeys = MetricInfoPanel.RENDERED_FIELD_KEYS;

    // Keys that are rendered separately (as title/header), not in the section list
    const separatelyRendered = ['metric_id', 'metric_title'];
    const requiredSectionKeys = DEFINITION_KEYS.filter(
      (k) => !separatelyRendered.includes(k),
    );

    fc.assert(
      fc.property(metricDefinitionArb, (definition) => {
        // Verify every required section key is in the rendered set
        for (const key of requiredSectionKeys) {
          expect(renderedKeys).toContain(key);
        }

        // Verify the definition object has all keys that will be rendered
        for (const key of renderedKeys) {
          expect(definition).toHaveProperty(key);
        }
      }),
      { numRuns: 100 },
    );
  });
});

// ---------------------------------------------------------------------------
// Property 6: Definitions schema validation — all entries have required fields
// Validates: Requirements 6.2, 8.3, 8.4
// ---------------------------------------------------------------------------

describe('Property 6: Definitions schema validation — all entries have required fields', () => {
  test('every entry has all 14 keys, metric_id is non-empty string, optional fields are strings', () => {
    fc.assert(
      fc.property(
        fc.array(metricDefinitionArb, { minLength: 1, maxLength: 15 }),
        (definitions) => {
          for (const def of definitions) {
            // All 14 keys present
            for (const key of DEFINITION_KEYS) {
              expect(def).toHaveProperty(key);
            }

            // metric_id is a non-empty string
            expect(typeof def.metric_id).toBe('string');
            expect(def.metric_id.length).toBeGreaterThan(0);

            // Optional fields are strings (not null/undefined)
            const optionalFields = [
              'exclusions',
              'special_conditions',
              'notes',
            ];
            for (const field of optionalFields) {
              expect(typeof def[field]).toBe('string');
              expect(def[field]).not.toBeNull();
              expect(def[field]).not.toBeUndefined();
            }
          }
        },
      ),
      { numRuns: 100 },
    );
  });
});

// ---------------------------------------------------------------------------
// Property 7: Lookup map construction preserves all definitions
// Validates: Requirements 6.4
// ---------------------------------------------------------------------------

describe('Property 7: Lookup map construction preserves all definitions', () => {
  test('map size equals array length and every definition is retrievable', () => {
    fc.assert(
      fc.property(uniqueDefinitionsArb, (definitions) => {
        // Build lookup map
        const lookup = {};
        for (const def of definitions) {
          lookup[def.metric_id] = def;
        }

        // Map size equals array length
        expect(Object.keys(lookup).length).toBe(definitions.length);

        // Every definition is retrievable by its metric_id
        for (const def of definitions) {
          expect(lookup[def.metric_id]).toBe(def);
        }
      }),
      { numRuns: 100 },
    );
  });
});

// ---------------------------------------------------------------------------
// Property 8: JSON round-trip preserves metric definition data
// Validates: Requirements 8.1, 8.2
// ---------------------------------------------------------------------------

describe('Property 8: JSON round-trip preserves metric definition data', () => {
  test('JSON.parse(JSON.stringify(definition)) produces a deeply equal object', () => {
    fc.assert(
      fc.property(metricDefinitionArb, (definition) => {
        const roundTripped = JSON.parse(JSON.stringify(definition));
        expect(roundTripped).toEqual(definition);
      }),
      { numRuns: 100 },
    );
  });
});
