/**
 * Tests for src/data/generatedStats.ts
 *
 * Verifies the structure, content, and computed values of the
 * pre-generated GENERATED_STATS constant.  All tests are
 * deterministic (no network calls).
 */

import { describe, it, expect, beforeAll } from 'vitest';
import {
  GENERATED_STATS,
  type YearlyStats,
  type CategoryRanking,
  type PredictionYear,
  type MonthlyActivity,
} from './generatedStats.js';

// ─── Top-level metadata ───────────────────────────────────────────────────────

describe('GENERATED_STATS — top-level metadata', () => {
  it('should be defined', () => {
    expect(GENERATED_STATS).toBeDefined();
  });

  it('should have a generatedAt ISO timestamp', () => {
    expect(typeof GENERATED_STATS.generatedAt).toBe('string');
    expect(GENERATED_STATS.generatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
  });

  it('should have a valid coverage period', () => {
    const { from, to } = GENERATED_STATS.coveragePeriod;
    expect(typeof from).toBe('number');
    expect(typeof to).toBe('number');
    expect(Number.isInteger(from)).toBe(true);
    expect(Number.isInteger(to)).toBe(true);
    expect(from).toBeLessThanOrEqual(to);
  });

  it('should include a methodologyVersion', () => {
    expect(typeof GENERATED_STATS.methodologyVersion).toBe('string');
    expect(GENERATED_STATS.methodologyVersion.length).toBeGreaterThan(0);
  });

  it('should include a dataSource attribution', () => {
    expect(typeof GENERATED_STATS.dataSource).toBe('string');
    expect(GENERATED_STATS.dataSource).toContain('europarl');
  });
});

// ─── yearlyStats ─────────────────────────────────────────────────────────────

describe('GENERATED_STATS.yearlyStats', () => {
  it('should be a non-empty array', () => {
    expect(Array.isArray(GENERATED_STATS.yearlyStats)).toBe(true);
    expect(GENERATED_STATS.yearlyStats.length).toBeGreaterThan(0);
  });

  it('should cover every year in the coverage period', () => {
    const { from, to } = GENERATED_STATS.coveragePeriod;
    const years = GENERATED_STATS.yearlyStats.map((y) => y.year);
    for (let year = from; year <= to; year++) {
      expect(years).toContain(year);
    }
  });

  it('should have exactly one entry per year in the coverage period', () => {
    const { from, to } = GENERATED_STATS.coveragePeriod;
    expect(GENERATED_STATS.yearlyStats).toHaveLength(to - from + 1);
  });

  describe('each entry structure', () => {
    let sample: YearlyStats;
    beforeAll(() => {
      expect(GENERATED_STATS.yearlyStats.length).toBeGreaterThan(0);
      sample = GENERATED_STATS.yearlyStats[0]!;
    });

    it('should have a numeric year', () => {
      expect(typeof sample.year).toBe('number');
    });

    it('should have plenarySessions count', () => {
      expect(typeof sample.plenarySessions).toBe('number');
      expect(sample.plenarySessions).toBeGreaterThanOrEqual(0);
    });

    it('should have legislativeActsAdopted count', () => {
      expect(typeof sample.legislativeActsAdopted).toBe('number');
    });

    it('should have rollCallVotes count', () => {
      expect(typeof sample.rollCallVotes).toBe('number');
    });

    it('should have committeeMeetings count', () => {
      expect(typeof sample.committeeMeetings).toBe('number');
    });

    it('should have parliamentaryQuestions count', () => {
      expect(typeof sample.parliamentaryQuestions).toBe('number');
    });

    it('should have a monthlyActivity array with 12 entries', () => {
      expect(Array.isArray(sample.monthlyActivity)).toBe(true);
      expect(sample.monthlyActivity).toHaveLength(12);
    });

    it('monthly entries should have month 1–12', () => {
      const months = sample.monthlyActivity.map((m: MonthlyActivity) => m.month);
      for (let m = 1; m <= 12; m++) {
        expect(months).toContain(m);
      }
    });

    it('monthly plenarySessions should be non-negative integers', () => {
      for (const monthly of sample.monthlyActivity) {
        expect(Number.isInteger(monthly.plenarySessions)).toBe(true);
        expect(monthly.plenarySessions).toBeGreaterThanOrEqual(0);
      }
    });

    it('should have a parliamentaryTerm string', () => {
      expect(typeof sample.parliamentaryTerm).toBe('string');
    });
  });

  it('sum of monthly plenarySessions should equal annual plenarySessions', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const monthlySum = yearly.monthlyActivity.reduce(
        (acc, m) => acc + m.plenarySessions,
        0
      );
      expect(monthlySum).toBe(yearly.plenarySessions);
    }
  });

  it('sum of monthly legislativeActsAdopted should equal annual legislativeActsAdopted', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const monthlySum = yearly.monthlyActivity.reduce(
        (acc, m) => acc + m.legislativeActsAdopted,
        0
      );
      expect(monthlySum).toBe(yearly.legislativeActsAdopted);
    }
  });

  it('all years should have positive annual totals for key metrics', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      // Not all metrics may be present for every year, but at least some should be > 0
      const hasActivity =
        yearly.plenarySessions > 0 ||
        yearly.rollCallVotes > 0 ||
        yearly.legislativeActsAdopted > 0;
      expect(hasActivity).toBe(true);
    }
  });
});

// ─── categoryRankings ────────────────────────────────────────────────────────

describe('GENERATED_STATS.categoryRankings', () => {
  it('should be a non-empty array', () => {
    expect(Array.isArray(GENERATED_STATS.categoryRankings)).toBe(true);
    expect(GENERATED_STATS.categoryRankings.length).toBeGreaterThan(0);
  });

  describe('each ranking entry structure', () => {
    let sample: CategoryRanking;
    beforeAll(() => {
      expect(GENERATED_STATS.categoryRankings.length).toBeGreaterThan(0);
      sample = GENERATED_STATS.categoryRankings[0]!;
    });

    it('should have a category string', () => {
      expect(typeof sample.category).toBe('string');
    });

    it('should have a rankings array', () => {
      expect(Array.isArray(sample.rankings)).toBe(true);
      expect(sample.rankings.length).toBeGreaterThan(0);
    });

    it('rankings entries should have year, rank, percentile, and totalActivityScore', () => {
      for (const ranked of sample.rankings) {
        expect(typeof ranked.year).toBe('number');
        expect(typeof ranked.rank).toBe('number');
        expect(typeof ranked.percentile).toBe('number');
        expect(typeof ranked.totalActivityScore).toBe('number');
      }
    });

    it('should have numeric mean, stdDev, median', () => {
      expect(typeof sample.mean).toBe('number');
      expect(typeof sample.stdDev).toBe('number');
      expect(typeof sample.median).toBe('number');
    });

    it('should have topYear and bottomYear', () => {
      expect(typeof sample.topYear).toBe('number');
      expect(typeof sample.bottomYear).toBe('number');
    });
  });

  it('should include a Plenary Sessions category', () => {
    const categories = GENERATED_STATS.categoryRankings.map((r) => r.category);
    expect(categories).toContain('Plenary Sessions');
  });
});

// ─── predictions ─────────────────────────────────────────────────────────────

describe('GENERATED_STATS.predictions', () => {
  it('should be a non-empty array', () => {
    expect(Array.isArray(GENERATED_STATS.predictions)).toBe(true);
    expect(GENERATED_STATS.predictions.length).toBeGreaterThan(0);
  });

  it('should cover future years (beyond the coverage period)', () => {
    const { to } = GENERATED_STATS.coveragePeriod;
    const years = GENERATED_STATS.predictions.map((p) => p.year);
    for (const year of years) {
      expect(year).toBeGreaterThan(to);
    }
  });

  describe('each prediction entry structure', () => {
    let sample: PredictionYear;
    beforeAll(() => {
      expect(GENERATED_STATS.predictions.length).toBeGreaterThan(0);
      sample = GENERATED_STATS.predictions[0]!;
    });

    it('should have a numeric year', () => {
      expect(typeof sample.year).toBe('number');
    });

    it('should have predictedPlenarySessions', () => {
      expect(typeof sample.predictedPlenarySessions).toBe('number');
    });

    it('should have predictedLegislativeActs', () => {
      expect(typeof sample.predictedLegislativeActs).toBe('number');
    });

    it('should have predictedRollCallVotes', () => {
      expect(typeof sample.predictedRollCallVotes).toBe('number');
    });

    it('should have a confidenceInterval string', () => {
      expect(typeof sample.confidenceInterval).toBe('string');
    });

    it('should have a methodology string', () => {
      expect(typeof sample.methodology).toBe('string');
      expect(sample.methodology.length).toBeGreaterThan(0);
    });
  });
});

// ─── analysisSummary ─────────────────────────────────────────────────────────

describe('GENERATED_STATS.analysisSummary', () => {
  const summary = GENERATED_STATS.analysisSummary;

  it('should have overallTrend string', () => {
    expect(typeof summary.overallTrend).toBe('string');
    expect(summary.overallTrend.length).toBeGreaterThan(0);
  });

  it('should have a valid peakActivityYear', () => {
    const { from, to } = GENERATED_STATS.coveragePeriod;
    expect(typeof summary.peakActivityYear).toBe('number');
    expect(summary.peakActivityYear).toBeGreaterThanOrEqual(from);
    expect(summary.peakActivityYear).toBeLessThanOrEqual(to);
  });

  it('should have a valid lowestActivityYear', () => {
    const { from, to } = GENERATED_STATS.coveragePeriod;
    expect(typeof summary.lowestActivityYear).toBe('number');
    expect(summary.lowestActivityYear).toBeGreaterThanOrEqual(from);
    expect(summary.lowestActivityYear).toBeLessThanOrEqual(to);
  });

  it('peakActivityYear should have higher acts than lowestActivityYear', () => {
    const yearly = GENERATED_STATS.yearlyStats;
    const peakEntry = yearly.find((y) => y.year === summary.peakActivityYear);
    const lowestEntry = yearly.find((y) => y.year === summary.lowestActivityYear);
    expect(peakEntry?.legislativeActsAdopted).toBeGreaterThan(
      lowestEntry?.legislativeActsAdopted ?? Infinity
    );
  });

  it('should have a positive averageAnnualLegislativeOutput', () => {
    expect(typeof summary.averageAnnualLegislativeOutput).toBe('number');
    expect(summary.averageAnnualLegislativeOutput).toBeGreaterThan(0);
  });

  it('should have a legislativeProductivityTrend value', () => {
    expect(typeof summary.legislativeProductivityTrend).toBe('string');
    expect(['INCREASING', 'DECREASING', 'STABLE']).toContain(
      summary.legislativeProductivityTrend
    );
  });

  it('should have a non-empty keyFindings array', () => {
    expect(Array.isArray(summary.keyFindings)).toBe(true);
    expect(summary.keyFindings.length).toBeGreaterThan(0);
  });

  it('each keyFinding should be a non-empty string', () => {
    for (const finding of summary.keyFindings) {
      expect(typeof finding).toBe('string');
      expect(finding.length).toBeGreaterThan(0);
    }
  });
});

// ─── Data integrity ───────────────────────────────────────────────────────────

describe('GENERATED_STATS data integrity', () => {
  it('averageAnnualLegislativeOutput should match calculated average', () => {
    const yearly = GENERATED_STATS.yearlyStats;
    const avg = Math.round(
      yearly.reduce((s, y) => s + y.legislativeActsAdopted, 0) / yearly.length
    );
    expect(GENERATED_STATS.analysisSummary.averageAnnualLegislativeOutput).toBe(avg);
  });

  it('topYear should have highest total for that category', () => {
    const { from, to } = GENERATED_STATS.coveragePeriod;
    for (const ranking of GENERATED_STATS.categoryRankings) {
      expect(typeof ranking.topYear).toBe('number');
      expect(ranking.topYear).toBeGreaterThanOrEqual(from);
      expect(ranking.topYear).toBeLessThanOrEqual(to);
    }
  });
});

// ─── Political landscape data consistency ─────────────────────────────────────

describe('GENERATED_STATS — political landscape data consistency', () => {
  it('should have seat shares summing to approximately 100% for each year', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const totalShare = yearly.politicalLandscape.groups.reduce(
        (sum, g) => sum + g.seatShare,
        0
      );
      expect(totalShare).toBeGreaterThanOrEqual(98.5);
      expect(totalShare).toBeLessThanOrEqual(101.5);
    }
  });

  it('should have individual seatShare values consistent with seats/total calculation (±0.5%)', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const { groups } = yearly.politicalLandscape;
      const totalSeats = groups.reduce((sum, g) => sum + g.seats, 0);
      for (const g of groups) {
        const computedShare = Math.round((g.seats / totalSeats) * 1000) / 10;
        expect(Math.abs(computedShare - g.seatShare)).toBeLessThanOrEqual(0.5);
      }
    }
  });

  // Total seats may differ slightly from mepCount due to vacancies,
  // deaths, or transitional periods between group assignments. A ±5
  // tolerance reflects genuine parliamentary reality.
  it('should have total seats across all groups within ±5 of mepCount', () => {
    const MAX_SEAT_TOLERANCE = 5;
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const totalSeats = yearly.politicalLandscape.groups.reduce(
        (sum, g) => sum + g.seats,
        0
      );
      const diff = Math.abs(totalSeats - yearly.mepCount);
      expect(diff).toBeLessThanOrEqual(MAX_SEAT_TOLERANCE);
    }
  });

  it('should have largestGroup matching the group with the most seats', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const { groups, largestGroup } = yearly.politicalLandscape;
      const maxSeats = Math.max(...groups.map((g) => g.seats));
      const groupWithMostSeats = groups.find((g) => g.seats === maxSeats);
      expect(groupWithMostSeats).toBeDefined();
      expect(largestGroup).toBe(groupWithMostSeats!.name);
    }
  });

  it('should have largestGroupSeatShare matching the actual largest group seatShare', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const { groups, largestGroup, largestGroupSeatShare } = yearly.politicalLandscape;
      const largest = groups.find((g) => g.name === largestGroup);
      expect(largest).toBeDefined();
      expect(largestGroupSeatShare).toBe(largest!.seatShare);
    }
  });

  // NI (Non-Inscrits/Non-attached) members are MEPs not affiliated with any
  // formal political group. They are listed in `groups` for completeness but
  // are excluded from `totalGroups`, which counts only recognised groups.
  it('should have totalGroups equal to the number of non-NI groups', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const { groups, totalGroups } = yearly.politicalLandscape;
      const nonNiCount = groups.filter((g) => g.name !== 'NI').length;
      expect(totalGroups).toBe(nonNiCount);
    }
  });

  // The European Parliament always has non-affiliated (NI) MEPs. Every year
  // must include an NI entry in groups so seat totals are complete.
  it('should always include NI (Non-Inscrits) group in every year', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const ni = yearly.politicalLandscape.groups.find((g) => g.name === 'NI');
      expect(ni).toBeDefined();
      expect(ni!.seats).toBeGreaterThan(0);
      expect(ni!.seatShare).toBeGreaterThan(0);
    }
  });

  it('should have fragmentationIndex (ENPP) consistent with seat shares (±0.15 tolerance)', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const { groups, fragmentationIndex } = yearly.politicalLandscape;
      // Laakso-Taagepera ENPP = 1 / sum(pi^2) where pi = seatShare / 100
      const sumSquares = groups.reduce(
        (sum, g) => sum + (g.seatShare / 100) ** 2,
        0
      );
      const expectedENPP = sumSquares > 0 ? 1 / sumSquares : 0;
      expect(Math.abs(fragmentationIndex - expectedENPP)).toBeLessThanOrEqual(0.15);
    }
  });

  it('should have grandCoalitionPossible true when top 2 groups exceed 50% of mepCount seats', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const { groups, grandCoalitionPossible } = yearly.politicalLandscape;
      const sortedBySeats = [...groups].sort((a, b) => b.seats - a.seats);
      const top2Seats = (sortedBySeats[0]?.seats ?? 0) + (sortedBySeats[1]?.seats ?? 0);
      const majorityThreshold = yearly.mepCount / 2;

      if (top2Seats > majorityThreshold) {
        expect(grandCoalitionPossible).toBe(true);
      }
      // Note: when top2Seats <= majorityThreshold, grandCoalitionPossible should be false
      // but we only assert the positive direction to avoid false negatives from rounding
    }
  });

  it('should have mepCount at least as large as the largest group seat count', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const maxGroupSeats = Math.max(
        ...yearly.politicalLandscape.groups.map((g) => g.seats)
      );
      expect(yearly.mepCount).toBeGreaterThanOrEqual(maxGroupSeats);
    }
  });

  it('should have mepCount within a plausible range for the EU parliament (600–800)', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      expect(yearly.mepCount).toBeGreaterThanOrEqual(600);
      expect(yearly.mepCount).toBeLessThanOrEqual(800);
    }
  });

  it('should have all group seats as non-negative integers', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      for (const g of yearly.politicalLandscape.groups) {
        expect(Number.isInteger(g.seats)).toBe(true);
        expect(g.seats).toBeGreaterThanOrEqual(0);
      }
    }
  });

  it('should have all seatShare values between 0 and 100', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      for (const g of yearly.politicalLandscape.groups) {
        expect(g.seatShare).toBeGreaterThanOrEqual(0);
        expect(g.seatShare).toBeLessThanOrEqual(100);
      }
    }
  });
});

// ─── Monthly distribution integrity ──────────────────────────────────────────

describe('GENERATED_STATS — monthly distribution integrity', () => {
  const MONTHLY_METRICS = [
    'plenarySessions',
    'legislativeActsAdopted',
    'rollCallVotes',
    'committeeMeetings',
    'parliamentaryQuestions',
    'resolutions',
    'speeches',
    'adoptedTexts',
    'procedures',
    'events',
    'documents',
    'mepTurnover',
    'declarations',
  ] as const;

  it('should have monthly sums equal to annual totals for all 13 metrics', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      for (const metric of MONTHLY_METRICS) {
        const monthlySum = yearly.monthlyActivity.reduce(
          (acc, m) => acc + m[metric],
          0
        );
        const annualTotal = yearly[metric];
        expect(monthlySum).toBe(annualTotal);
      }
    }
  });

  // Metrics that use synthetic distribution (not available via monthly API batching)
  // always have August as the lowest month due to parliamentary recess weights.
  // Metrics with real monthly data from the EP API may not follow this pattern.
  const SYNTHETIC_ONLY_METRICS = [
    'legislativeActsAdopted',
    'rollCallVotes',
    'committeeMeetings',
    'parliamentaryQuestions',
    'resolutions',
    'adoptedTexts',
    'procedures',
    'documents',
    'mepTurnover',
    'declarations',
  ] as const;

  it('should have August (month 8) as the lowest activity month for synthetic metrics', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      const augustEntry = yearly.monthlyActivity.find((m) => m.month === 8);
      expect(augustEntry).toBeDefined();

      for (const metric of SYNTHETIC_ONLY_METRICS) {
        const augustValue = augustEntry![metric];
        for (const m of yearly.monthlyActivity) {
          if (m.month !== 8) {
            expect(augustValue).toBeLessThanOrEqual(m[metric]);
          }
        }
      }
    }
  });

  it('should have non-negative values for all monthly metrics', () => {
    for (const yearly of GENERATED_STATS.yearlyStats) {
      for (const monthly of yearly.monthlyActivity) {
        for (const metric of MONTHLY_METRICS) {
          expect(monthly[metric]).toBeGreaterThanOrEqual(0);
        }
      }
    }
  });
});

// ─── Parliamentary term assignment ───────────────────────────────────────────

describe('GENERATED_STATS — parliamentary term assignment', () => {
  const findYearEntry = (year: number) =>
    GENERATED_STATS.yearlyStats.find((y) => y.year === year);

  it('should assign EP6 to years 2004–2008', () => {
    for (const year of [2004, 2005, 2006, 2007, 2008]) {
      const entry = findYearEntry(year);
      expect(entry).toBeDefined();
      expect(entry!.parliamentaryTerm).toContain('EP6');
    }
  });

  it('should assign EP6 or EP7 to year 2009 (transition)', () => {
    const entry = findYearEntry(2009);
    expect(entry).toBeDefined();
    const term = entry!.parliamentaryTerm;
    expect(term.includes('EP6') || term.includes('EP7')).toBe(true);
  });

  it('should assign EP7 to years 2010–2013', () => {
    for (const year of [2010, 2011, 2012, 2013]) {
      const entry = findYearEntry(year);
      expect(entry).toBeDefined();
      expect(entry!.parliamentaryTerm).toContain('EP7');
    }
  });

  it('should assign EP7 or EP8 to year 2014 (transition)', () => {
    const entry = findYearEntry(2014);
    expect(entry).toBeDefined();
    const term = entry!.parliamentaryTerm;
    expect(term.includes('EP7') || term.includes('EP8')).toBe(true);
  });

  it('should assign EP8 to years 2015–2018', () => {
    for (const year of [2015, 2016, 2017, 2018]) {
      const entry = findYearEntry(year);
      expect(entry).toBeDefined();
      expect(entry!.parliamentaryTerm).toContain('EP8');
    }
  });

  it('should assign EP8 or EP9 to year 2019 (transition)', () => {
    const entry = findYearEntry(2019);
    expect(entry).toBeDefined();
    const term = entry!.parliamentaryTerm;
    expect(term.includes('EP8') || term.includes('EP9')).toBe(true);
  });

  it('should assign EP9 to years 2020–2023', () => {
    for (const year of [2020, 2021, 2022, 2023]) {
      const entry = findYearEntry(year);
      expect(entry).toBeDefined();
      expect(entry!.parliamentaryTerm).toContain('EP9');
    }
  });

  it('should assign EP9 or EP10 to year 2024 (transition)', () => {
    const entry = findYearEntry(2024);
    expect(entry).toBeDefined();
    const term = entry!.parliamentaryTerm;
    expect(term.includes('EP9') || term.includes('EP10')).toBe(true);
  });

  it('should assign EP10 to years 2025–2026', () => {
    for (const year of [2025, 2026]) {
      const entry = findYearEntry(year);
      expect(entry).toBeDefined();
      expect(entry!.parliamentaryTerm).toContain('EP10');
    }
  });
});

// ─── Prediction integrity ────────────────────────────────────────────────────

describe('GENERATED_STATS — prediction integrity', () => {
  const PREDICTION_FIELDS = [
    'predictedPlenarySessions',
    'predictedLegislativeActs',
    'predictedRollCallVotes',
    'predictedCommitteeMeetings',
    'predictedParliamentaryQuestions',
    'predictedResolutions',
    'predictedSpeeches',
    'predictedAdoptedTexts',
    'predictedProcedures',
    'predictedEvents',
    'predictedDocuments',
    'predictedMepTurnover',
    'predictedDeclarations',
  ] as const;

  it('should have all predicted values positive', () => {
    for (const prediction of GENERATED_STATS.predictions) {
      for (const field of PREDICTION_FIELDS) {
        expect(prediction[field]).toBeGreaterThan(0);
      }
    }
  });

  it('should have prediction years sequential from 2027 to 2031', () => {
    const years = GENERATED_STATS.predictions.map((p) => p.year);
    expect(years).toEqual([2027, 2028, 2029, 2030, 2031]);
  });

  it('should have each prediction include all 13 metric fields', () => {
    for (const prediction of GENERATED_STATS.predictions) {
      for (const field of PREDICTION_FIELDS) {
        expect(prediction).toHaveProperty(field);
        expect(typeof prediction[field]).toBe('number');
      }
    }
  });
});

// ─── Derived intelligence metrics ─────────────────────────────────────────────

describe('GENERATED_STATS — derived intelligence metrics', () => {
  let allYears: YearlyStats[];
  beforeAll(() => {
    allYears = GENERATED_STATS.yearlyStats;
  });

  it('every year should have a derivedIntelligence object', () => {
    for (const y of allYears) {
      expect(y.derivedIntelligence).toBeDefined();
    }
  });

  describe('legislative efficiency', () => {
    it('legislativeOutputPerSession should be acts / sessions', () => {
      for (const y of allYears) {
        const di = y.derivedIntelligence;
        const expected = y.plenarySessions === 0
          ? 0
          : Math.round((y.legislativeActsAdopted / y.plenarySessions) * 100) / 100;
        expect(di.legislativeOutputPerSession).toBeCloseTo(expected, 1);
      }
    });

    it('legislativeOutputPerMEP should be acts / mepCount', () => {
      for (const y of allYears) {
        const di = y.derivedIntelligence;
        const expected = y.mepCount === 0
          ? 0
          : Math.round((y.legislativeActsAdopted / y.mepCount) * 1000) / 1000;
        expect(di.legislativeOutputPerMEP).toBeCloseTo(expected, 2);
      }
    });

    it('rollCallVoteYield should be between 0 and 100', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.rollCallVoteYield).toBeGreaterThanOrEqual(0);
        expect(y.derivedIntelligence.rollCallVoteYield).toBeLessThanOrEqual(100);
      }
    });

    it('procedureCompletionRate should be between 0 and 100', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.procedureCompletionRate).toBeGreaterThanOrEqual(0);
        expect(y.derivedIntelligence.procedureCompletionRate).toBeLessThanOrEqual(100);
      }
    });

    it('documentBurdenPerAct should be positive', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.documentBurdenPerAct).toBeGreaterThan(0);
      }
    });
  });

  describe('parliamentary engagement', () => {
    it('mepOversightIntensity should be questions / mepCount', () => {
      for (const y of allYears) {
        const expected = y.mepCount === 0
          ? 0
          : Math.round((y.parliamentaryQuestions / y.mepCount) * 100) / 100;
        expect(y.derivedIntelligence.mepOversightIntensity).toBeCloseTo(expected, 1);
      }
    });

    it('mepSpeechRate should be positive', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.mepSpeechRate).toBeGreaterThan(0);
      }
    });

    it('debateIntensityPerSession should be speeches / sessions', () => {
      for (const y of allYears) {
        const di = y.derivedIntelligence;
        expect(di.debateIntensityPerSession).toBeGreaterThan(0);
      }
    });
  });

  describe('political concentration', () => {
    it('topTwoGroupsConcentration should be sum of two largest recognised group shares (excluding NI)', () => {
      for (const y of allYears) {
        const sorted = [...y.politicalLandscape.groups]
          .filter((g) => g.name !== 'NI')
          .sort((a, b) => b.seatShare - a.seatShare);
        const expected = (sorted[0]?.seatShare ?? 0) + (sorted[1]?.seatShare ?? 0);
        expect(y.derivedIntelligence.topTwoGroupsConcentration).toBeCloseTo(expected, 0);
      }
    });

    it('CR2 should be below 50 from 2019 onward (no grand coalition majority)', () => {
      const post2019 = allYears.filter((y) => y.year >= 2019);
      for (const y of post2019) {
        expect(y.derivedIntelligence.topTwoGroupsConcentration).toBeLessThan(50);
      }
    });

    it('HHI should be between 0 and 1', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.herfindahlHirschmanIndex).toBeGreaterThan(0);
        expect(y.derivedIntelligence.herfindahlHirschmanIndex).toBeLessThan(1);
      }
    });

    it('minimumWinningCoalitionSize should be at least 2 for all years', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.minimumWinningCoalitionSize).toBeGreaterThanOrEqual(2);
      }
    });

    it('MWC should be 3+ from 2019 onward', () => {
      const post2019 = allYears.filter((y) => y.year >= 2019);
      for (const y of post2019) {
        expect(y.derivedIntelligence.minimumWinningCoalitionSize).toBeGreaterThanOrEqual(3);
      }
    });

    it('grandCoalitionSurplusDeficit should match CR2 - 50', () => {
      for (const y of allYears) {
        const di = y.derivedIntelligence;
        expect(di.grandCoalitionSurplusDeficit).toBeCloseTo(
          di.topTwoGroupsConcentration - 50,
          0,
        );
      }
    });

    it('dominanceRatio should be >= 1 (largest group is always dominant)', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.dominanceRatio).toBeGreaterThanOrEqual(1);
      }
    });
  });

  describe('political compass (multi-dimensional)', () => {
    it('every year should have politicalCompass within politicalBlocAnalysis', () => {
      for (const y of allYears) {
        const compass = y.derivedIntelligence.politicalBlocAnalysis.politicalCompass;
        expect(compass).toBeDefined();
        expect(compass.parliamentEconomicPosition).toBeGreaterThan(0);
        expect(compass.parliamentEconomicPosition).toBeLessThan(10);
        expect(compass.parliamentSocialPosition).toBeGreaterThan(0);
        expect(compass.parliamentSocialPosition).toBeLessThan(10);
        expect(compass.parliamentEuPosition).toBeGreaterThan(0);
        expect(compass.parliamentEuPosition).toBeLessThan(10);
      }
    });

    it('quadrant distribution should sum to approximately 100% (minus NI)', () => {
      for (const y of allYears) {
        const qd = y.derivedIntelligence.politicalBlocAnalysis.politicalCompass.quadrantDistribution;
        const total = qd.libertarianLeft + qd.libertarianRight + qd.authoritarianLeft + qd.authoritarianRight;
        // Should be close to 100 minus NI share
        const niShare = y.derivedIntelligence.nonAttachedShare;
        expect(total).toBeCloseTo(100 - niShare, 0);
      }
    });

    it('dominantQuadrant should be a valid quadrant', () => {
      const validQuadrants = ['libertarianLeft', 'libertarianRight', 'authoritarianLeft', 'authoritarianRight'];
      for (const y of allYears) {
        expect(validQuadrants).toContain(
          y.derivedIntelligence.politicalBlocAnalysis.politicalCompass.dominantQuadrant,
        );
      }
    });

    it('economicPolarisation should be positive', () => {
      for (const y of allYears) {
        expect(
          y.derivedIntelligence.politicalBlocAnalysis.politicalCompass.economicPolarisation,
        ).toBeGreaterThan(0);
      }
    });

    it('authLibTension should be positive', () => {
      for (const y of allYears) {
        expect(
          y.derivedIntelligence.politicalBlocAnalysis.politicalCompass.authLibTension,
        ).toBeGreaterThan(0);
      }
    });

    it('EP10 (2024-2026) should show higher authoritarian-right share than EP6 (2004)', () => {
      const ep6Start = allYears.find((y) => y.year === 2004);
      const ep10 = allYears.find((y) => y.year === 2026);
      expect(ep6Start).toBeDefined();
      expect(ep10).toBeDefined();
      const ep6AuthRight = ep6Start!.derivedIntelligence.politicalBlocAnalysis.politicalCompass.quadrantDistribution.authoritarianRight;
      const ep10AuthRight = ep10!.derivedIntelligence.politicalBlocAnalysis.politicalCompass.quadrantDistribution.authoritarianRight;
      expect(ep10AuthRight).toBeGreaterThan(ep6AuthRight);
    });
  });

  describe('traditional bloc shares', () => {
    it('left + centre + right shares should not exceed 100', () => {
      for (const y of allYears) {
        const bloc = y.derivedIntelligence.politicalBlocAnalysis;
        // Note: shares won't sum to 100 exactly due to NI exclusion
        expect(bloc.leftBlocShare + bloc.centreBlocShare + bloc.rightBlocShare).toBeLessThanOrEqual(100);
      }
    });

    it('bipolarIndex should be between -1 and 1', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.politicalBlocAnalysis.bipolarIndex).toBeGreaterThanOrEqual(-1);
        expect(y.derivedIntelligence.politicalBlocAnalysis.bipolarIndex).toBeLessThanOrEqual(1);
      }
    });

    it('euroscepticShare should be higher in EP10 than EP6', () => {
      const ep6 = allYears.find((y) => y.year === 2004);
      const ep10 = allYears.find((y) => y.year === 2026);
      expect(ep6).toBeDefined();
      expect(ep10).toBeDefined();
      expect(ep10!.derivedIntelligence.politicalBlocAnalysis.euroscepticShare)
        .toBeGreaterThan(ep6!.derivedIntelligence.politicalBlocAnalysis.euroscepticShare);
    });
  });

  describe('institutional stability', () => {
    it('mepStabilityIndex should be between 0 and 1', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.mepStabilityIndex).toBeGreaterThanOrEqual(0);
        expect(y.derivedIntelligence.mepStabilityIndex).toBeLessThanOrEqual(1);
      }
    });

    it('election years should have HIGH institutional memory risk', () => {
      const electionYears = allYears.filter((y) =>
        [2004, 2009, 2014, 2019, 2024].includes(y.year),
      );
      for (const y of electionYears) {
        expect(y.derivedIntelligence.institutionalMemoryRisk).toBe('HIGH');
      }
    });

    it('non-election years should have LOW institutional memory risk', () => {
      const nonElection = allYears.filter(
        (y) => ![2004, 2007, 2009, 2014, 2019, 2020, 2024].includes(y.year),
      );
      for (const y of nonElection) {
        expect(y.derivedIntelligence.institutionalMemoryRisk).toBe('LOW');
      }
    });

    it('turnoverRate + (1 - stabilityIndex)*100 should be consistent', () => {
      for (const y of allYears) {
        const di = y.derivedIntelligence;
        expect(di.turnoverRate).toBeCloseTo((1 - di.mepStabilityIndex) * 100, 0);
      }
    });
  });

  describe('year-over-year dynamics', () => {
    it('first year (2004) should have null YoY values', () => {
      const first = allYears[0]!;
      expect(first.derivedIntelligence.legislativeOutputChange).toBeNull();
      expect(first.derivedIntelligence.fragmentationVelocity).toBeNull();
      expect(first.derivedIntelligence.questionsChange).toBeNull();
    });

    it('all subsequent years should have non-null YoY values', () => {
      for (const y of allYears.slice(1)) {
        expect(y.derivedIntelligence.legislativeOutputChange).not.toBeNull();
        expect(y.derivedIntelligence.fragmentationVelocity).not.toBeNull();
        expect(y.derivedIntelligence.questionsChange).not.toBeNull();
      }
    });

    it('election years should show negative legislative output change', () => {
      const transitions = allYears.filter((y) =>
        [2009, 2014, 2019, 2024].includes(y.year),
      );
      for (const y of transitions) {
        expect(y.derivedIntelligence.legislativeOutputChange).not.toBeNull();
        expect(y.derivedIntelligence.legislativeOutputChange!).toBeLessThan(0);
      }
    });
  });

  describe('composite indices', () => {
    it('oversightToLegislationBalance should be questions/acts', () => {
      for (const y of allYears) {
        const expected = y.legislativeActsAdopted === 0
          ? 0
          : Math.round((y.parliamentaryQuestions / y.legislativeActsAdopted) * 10) / 10;
        expect(y.derivedIntelligence.oversightToLegislationBalance).toBeCloseTo(expected, 0);
      }
    });

    it('speechToVoteRatio should be positive', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.speechToVoteRatio).toBeGreaterThan(0);
      }
    });

    it('committeeToPlenaryRatio should be positive', () => {
      for (const y of allYears) {
        expect(y.derivedIntelligence.committeeToPlenaryRatio).toBeGreaterThan(0);
      }
    });
  });
});

// ─── OSINT key findings in analysis summary ───────────────────────────────────

describe('GENERATED_STATS — OSINT key findings', () => {
  it('analysisSummary should include OSINT-derived findings', () => {
    const findings = GENERATED_STATS.analysisSummary.keyFindings;
    const osintFindings = findings.filter((f) => f.startsWith('OSINT:'));
    expect(osintFindings.length).toBeGreaterThanOrEqual(6);
  });

  it('should mention CR₂ structural regime change', () => {
    const findings = GENERATED_STATS.analysisSummary.keyFindings;
    expect(findings.some((f) => f.includes('CR₂'))).toBe(true);
  });

  it('should mention minimum winning coalition', () => {
    const findings = GENERATED_STATS.analysisSummary.keyFindings;
    expect(findings.some((f) => f.includes('Minimum winning coalition'))).toBe(true);
  });

  it('should mention bipolar index shift', () => {
    const findings = GENERATED_STATS.analysisSummary.keyFindings;
    expect(findings.some((f) => f.includes('Bipolar index'))).toBe(true);
  });

  it('should mention HHI deconcentration', () => {
    const findings = GENERATED_STATS.analysisSummary.keyFindings;
    expect(findings.some((f) => f.includes('Herfindahl-Hirschman'))).toBe(true);
  });
});
