/**
 * Intelligent A/B Testing — Unit Tests.
 * Bucketing, statistics, Bayesian inference, early stopping, guardrails.
 */

import { describe, it, expect } from 'vitest';
import { fnv1a, assignVariant, isInExperimentBucket, assignByStrategy } from './bucketing.js';
import {
  betaFromVariant,
  betaCredibleInterval,
  probabilityVariantBeatsControl,
  probabilityVariantBeatsAll,
  expectedLossIfChosen,
  checkEarlyStopping,
  generateExperimentResult,
  validateAA,
  calculateSampleSize,
} from './statistics.js';
import { runGuardrails, canAutoPromote } from './guardrails.js';
import { matchesTargeting } from './targeting.js';
import type { VariantDoc, ExperimentDoc } from './types.js';

// ─────────────────────────────────────────────────────────────────────────────
// Bucketing Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('FNV-1a Hash', () => {
  it('produces deterministic output', () => {
    const h1 = fnv1a('test-string');
    const h2 = fnv1a('test-string');
    expect(h1).toBe(h2);
  });

  it('produces different hashes for different inputs', () => {
    const h1 = fnv1a('input-a');
    const h2 = fnv1a('input-b');
    expect(h1).not.toBe(h2);
  });

  it('produces 32-bit unsigned integers', () => {
    const h = fnv1a('any-string');
    expect(h).toBeGreaterThanOrEqual(0);
    expect(h).toBeLessThanOrEqual(0xffffffff);
  });
});

describe('assignVariant', () => {
  const variants = [
    { key: 'control', weight: 50 },
    { key: 'variant_a', weight: 50 },
  ];

  it('assigns deterministically', () => {
    const v1 = assignVariant('exp-1', 'user-a', variants);
    const v2 = assignVariant('exp-1', 'user-a', variants);
    expect(v1).toBe(v2);
  });

  it('distributes across variants', () => {
    const assignments = new Set<string>();
    for (let i = 0; i < 100; i++) {
      assignments.add(assignVariant('exp-1', `user-${i}`, variants));
    }
    expect(assignments.size).toBe(2);
  });

  it('respects uneven weights', () => {
    const unevenVariants = [
      { key: 'control', weight: 90 },
      { key: 'variant_a', weight: 10 },
    ];
    let controlCount = 0;
    for (let i = 0; i < 1000; i++) {
      if (assignVariant('exp-2', `u-${i}`, unevenVariants) === 'control') {
        controlCount++;
      }
    }
    expect(controlCount).toBeGreaterThan(700);
    expect(controlCount).toBeLessThan(990);
  });
});

describe('isInExperimentBucket', () => {
  it('includes correct percentage of users', () => {
    let included = 0;
    for (let i = 0; i < 1000; i++) {
      if (isInExperimentBucket('exp-1', `user-${i}`, 50)) {
        included++;
      }
    }
    // Should be roughly 50% (allow wide margin for hash distribution)
    expect(included).toBeGreaterThan(400);
    expect(included).toBeLessThan(600);
  });

  it('is deterministic for same user/experiment', () => {
    const r1 = isInExperimentBucket('exp-1', 'user-a', 50);
    const r2 = isInExperimentBucket('exp-1', 'user-a', 50);
    expect(r1).toBe(r2);
  });

  it('excludes all users at 0%', () => {
    for (let i = 0; i < 100; i++) {
      expect(isInExperimentBucket('exp-1', `user-${i}`, 0)).toBe(false);
    }
  });

  it('includes all users at 100%', () => {
    for (let i = 0; i < 100; i++) {
      expect(isInExperimentBucket('exp-1', `user-${i}`, 100)).toBe(true);
    }
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Targeting Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('matchesTargeting', () => {
  it('matches when no targeting specified', () => {
    expect(matchesTargeting({ platform: 'ios' }, {})).toBe(true);
  });

  it('matches platform', () => {
    expect(matchesTargeting({ platform: 'ios' }, { platforms: ['ios'] })).toBe(true);
    expect(matchesTargeting({ platform: 'android' }, { platforms: ['ios'] })).toBe(false);
  });

  it('matches version range', () => {
    expect(matchesTargeting({ appVersion: '1.5.0' }, { appVersions: { min: '1.0.0' } })).toBe(true);
    expect(matchesTargeting({ appVersion: '0.5.0' }, { appVersions: { min: '1.0.0' } })).toBe(
      false
    );
  });

  it('matches user segments', () => {
    expect(matchesTargeting({ userSegments: ['pro'] }, { userSegments: ['pro'] })).toBe(true);
    expect(matchesTargeting({ userSegments: ['free'] }, { userSegments: ['pro'] })).toBe(false);
  });

  it('matches user properties', () => {
    expect(
      matchesTargeting(
        { userProperties: { tier: 'premium' } },
        { userProperties: { tier: 'premium' } }
      )
    ).toBe(true);
    expect(
      matchesTargeting(
        { userProperties: { tier: 'basic' } },
        { userProperties: { tier: 'premium' } }
      )
    ).toBe(false);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Bayesian Statistics Tests
// ─────────────────────────────────────────────────────────────────────────────

function createMockVariant(overrides: Partial<VariantDoc> = {}): VariantDoc {
  return {
    id: 'var_test',
    experimentId: 'exp_test',
    name: 'Test Variant',
    description: '',
    isControl: false,
    flagConfig: {},
    currentAllocationPercent: 50,
    stats: {
      participants: 100,
      events: 50,
      primaryMetricValue: 0.1,
      primaryMetricStdDev: 0.05,
      conversions: 10,
      conversionRate: 0.1,
      betaAlpha: 11,
      betaBeta: 91,
    },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    ...overrides,
  };
}

function createMockExperiment(overrides: Partial<ExperimentDoc> = {}): ExperimentDoc {
  return {
    id: 'exp_test',
    productId: 'test_product',
    name: 'Test Experiment',
    description: '',
    hypothesis: 'Test hypothesis',
    status: 'running',
    controlVariantId: 'var_control',
    variantIds: ['var_control', 'var_test'],
    allocationStrategy: 'random',
    targetPercent: 100,
    targeting: {},
    primaryMetric: {
      name: 'conversion',
      type: 'conversion',
      eventName: 'purchase',
      aggregation: 'count',
      direction: 'increase',
      minimumDetectableEffect: 5,
    },
    secondaryMetrics: [],
    guardrails: {
      minSampleSizePerVariant: 100,
      maxDurationDays: 30,
      autoStopEnabled: true,
      winnerThreshold: 95,
      requireApprovalFor: 'none',
    },
    totalParticipants: 200,
    totalEvents: 100,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    ...overrides,
  };
}

describe('Beta Distribution', () => {
  it('computes correct parameters from variant', () => {
    const variant = createMockVariant({
      stats: {
        participants: 100,
        events: 50,
        primaryMetricValue: 0.1,
        conversions: 10,
        conversionRate: 0.1,
      },
    });
    const beta = betaFromVariant(variant);
    expect(beta.alpha).toBe(11); // conversions + 1
    expect(beta.beta).toBe(91); // failures + 1
  });

  it('generates credible interval', () => {
    const interval = betaCredibleInterval(11, 91);
    expect(interval.lower).toBeGreaterThan(0);
    expect(interval.lower).toBeLessThan(interval.mean);
    expect(interval.mean).toBeLessThan(interval.upper);
    expect(interval.upper).toBeLessThan(1);
  });

  it('has mean close to conversion rate', () => {
    const interval = betaCredibleInterval(11, 91);
    // Mean of Beta(11, 91) = 11 / (11 + 91) ≈ 0.108
    expect(interval.mean).toBeCloseTo(0.108, 2);
  });
});

describe('Probability Calculations', () => {
  it('calculates probability variant beats control', () => {
    const control = createMockVariant({
      stats: {
        participants: 100,
        events: 50,
        primaryMetricValue: 0.1,
        conversions: 10,
        conversionRate: 0.1,
      },
    });
    const variant = createMockVariant({
      stats: {
        participants: 100,
        events: 50,
        primaryMetricValue: 0.2,
        conversions: 20,
        conversionRate: 0.2,
      },
    });

    const prob = probabilityVariantBeatsControl(variant, control, 'conversion', 5000);
    // Variant with 20% conversion should beat control with 10% conversion
    expect(prob).toBeGreaterThan(0.8);
  });

  it('calculates probability variant beats all', () => {
    const variants = [
      createMockVariant({
        id: 'v1',
        stats: {
          participants: 100,
          events: 50,
          primaryMetricValue: 0.1,
          conversions: 10,
          conversionRate: 0.1,
        },
      }),
      createMockVariant({
        id: 'v2',
        stats: {
          participants: 100,
          events: 50,
          primaryMetricValue: 0.2,
          conversions: 20,
          conversionRate: 0.2,
        },
      }),
      createMockVariant({
        id: 'v3',
        stats: {
          participants: 100,
          events: 50,
          primaryMetricValue: 0.15,
          conversions: 15,
          conversionRate: 0.15,
        },
      }),
    ];

    const prob = probabilityVariantBeatsAll(variants[1], variants, 'conversion', 5000);
    // v2 (20%) should beat both v1 (10%) and v3 (15%)
    expect(prob).toBeGreaterThan(0.5);
  });

  it('calculates expected loss', () => {
    const variants = [
      createMockVariant({
        stats: {
          participants: 100,
          events: 50,
          primaryMetricValue: 0.1,
          conversions: 10,
          conversionRate: 0.1,
        },
      }),
      createMockVariant({
        stats: {
          participants: 100,
          events: 50,
          primaryMetricValue: 0.1,
          conversions: 10,
          conversionRate: 0.1,
        },
      }),
    ];

    const loss = expectedLossIfChosen(variants[0], variants, 'conversion', 5000);
    // Expected loss should be small for identical variants
    expect(loss).toBeLessThan(0.05);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Early Stopping Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('checkEarlyStopping', () => {
  it('does not stop before minimum sample size', () => {
    const experiment = createMockExperiment();
    const variants = [
      createMockVariant({
        isControl: true,
        stats: {
          participants: 50,
          events: 25,
          primaryMetricValue: 0.1,
          conversions: 5,
          conversionRate: 0.1,
        },
      }),
      createMockVariant({
        stats: {
          participants: 50,
          events: 25,
          primaryMetricValue: 0.5,
          conversions: 25,
          conversionRate: 0.5,
        },
      }),
    ];

    const result = checkEarlyStopping(experiment, variants, 5);
    expect(result.shouldStop).toBe(false);
  });

  it('stops when winner is clear', () => {
    const experiment = createMockExperiment();
    const variants = [
      createMockVariant({
        isControl: true,
        stats: {
          participants: 1000,
          events: 500,
          primaryMetricValue: 0.1,
          conversions: 100,
          conversionRate: 0.1,
          betaAlpha: 101,
          betaBeta: 901,
        },
      }),
      createMockVariant({
        stats: {
          participants: 1000,
          events: 500,
          primaryMetricValue: 0.5,
          conversions: 500,
          conversionRate: 0.5,
          betaAlpha: 501,
          betaBeta: 501,
        },
      }),
    ];

    const result = checkEarlyStopping(experiment, variants, 10);
    // 50% conversion rate should clearly beat 10% with enough samples
    expect(result.shouldStop || result.confidence > 0.8).toBe(true);
  });

  it('stops at max duration', () => {
    const experiment = createMockExperiment({
      guardrails: { ...createMockExperiment().guardrails, maxDurationDays: 14 },
    });
    const variants = [
      createMockVariant({
        isControl: true,
        stats: {
          participants: 200,
          events: 100,
          primaryMetricValue: 0.1,
          conversions: 20,
          conversionRate: 0.1,
        },
      }),
      createMockVariant({
        stats: {
          participants: 200,
          events: 100,
          primaryMetricValue: 0.11,
          conversions: 22,
          conversionRate: 0.11,
        },
      }),
    ];

    const result = checkEarlyStopping(experiment, variants, 30);
    expect(result.shouldStop).toBe(true);
    expect(result.reason).toContain('duration');
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Guardrails Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('runGuardrails', () => {
  it('requires minimum sample size', () => {
    const experiment = createMockExperiment();
    const variants = [
      createMockVariant({
        isControl: true,
        stats: {
          participants: 50,
          events: 25,
          primaryMetricValue: 0.1,
          conversions: 5,
          conversionRate: 0.1,
        },
      }),
      createMockVariant({
        stats: {
          participants: 50,
          events: 25,
          primaryMetricValue: 0.5,
          conversions: 25,
          conversionRate: 0.5,
        },
      }),
    ];

    const checks = runGuardrails(experiment, variants, 10, false);
    const sampleSizeCheck = checks.find(c => c.violation?.includes('sample'));
    expect(sampleSizeCheck?.passed).toBe(false);
    expect(sampleSizeCheck?.severity).toBe('blocking');
  });

  it('requires approval for revenue experiments when configured', () => {
    const experiment = createMockExperiment({
      guardrails: { ...createMockExperiment().guardrails, requireApprovalFor: 'revenue' },
      primaryMetric: { ...createMockExperiment().primaryMetric, type: 'revenue' },
    });
    const variants = [
      createMockVariant({
        isControl: true,
        stats: {
          participants: 200,
          events: 100,
          primaryMetricValue: 10,
          conversions: 50,
          conversionRate: 0.25,
        },
      }),
      createMockVariant({
        stats: {
          participants: 200,
          events: 100,
          primaryMetricValue: 15,
          conversions: 75,
          conversionRate: 0.375,
        },
      }),
    ];

    const checks = runGuardrails(experiment, variants, 10, true);
    const approvalCheck = checks.find(c => c.violation?.includes('Approval'));
    expect(approvalCheck?.passed).toBe(false);
  });
});

describe('canAutoPromote', () => {
  it('allows promotion when all checks pass', () => {
    const checks = [
      { passed: true, severity: 'info' as const },
      { passed: true, severity: 'info' as const },
    ];
    expect(canAutoPromote(checks)).toBe(true);
  });

  it('blocks promotion on blocking violations', () => {
    const checks = [
      { passed: true, severity: 'info' as const },
      { passed: false, violation: 'test', severity: 'blocking' as const },
    ];
    expect(canAutoPromote(checks)).toBe(false);
  });

  it('allows promotion with warnings only', () => {
    const checks = [
      { passed: true, severity: 'info' as const },
      { passed: false, violation: 'test', severity: 'warning' as const },
    ];
    expect(canAutoPromote(checks)).toBe(true);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Experiment Results Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('generateExperimentResult', () => {
  it('generates results with variant comparisons', () => {
    const experiment = createMockExperiment();
    const variants = [
      createMockVariant({
        id: 'var_control',
        isControl: true,
        name: 'Control',
        stats: {
          participants: 200,
          events: 100,
          primaryMetricValue: 0.1,
          conversions: 20,
          conversionRate: 0.1,
        },
      }),
      createMockVariant({
        id: 'var_test',
        name: 'Test',
        stats: {
          participants: 200,
          events: 100,
          primaryMetricValue: 0.15,
          conversions: 30,
          conversionRate: 0.15,
        },
      }),
    ];

    const result = generateExperimentResult(experiment, variants, 14);

    expect(result.experimentId).toBe(experiment.id);
    expect(result.variantResults).toHaveLength(2);
    expect(result.totalParticipants).toBe(experiment.totalParticipants);
    expect(result.daysRunning).toBe(14);
  });

  it('identifies winner when statistically significant', () => {
    const experiment = createMockExperiment();
    const variants = [
      createMockVariant({
        id: 'var_control',
        isControl: true,
        stats: {
          participants: 1000,
          events: 500,
          primaryMetricValue: 0.1,
          conversions: 100,
          conversionRate: 0.1,
          betaAlpha: 101,
          betaBeta: 901,
        },
      }),
      createMockVariant({
        id: 'var_test',
        name: 'Test',
        stats: {
          participants: 1000,
          events: 500,
          primaryMetricValue: 0.5,
          conversions: 500,
          conversionRate: 0.5,
          betaAlpha: 501,
          betaBeta: 501,
        },
      }),
    ];

    const result = generateExperimentResult(experiment, variants, 14);

    // Winner should be identified with high probability
    if (result.winnerVariantId) {
      expect(result.winnerProbability).toBeGreaterThan(0.8);
    }
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Validation Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('validateAA', () => {
  it('passes A/A test at reasonable rate', () => {
    const result = validateAA(100, 0.1, 500);
    // A/A tests should "pass" (not detect difference) at ~95% rate
    expect(result.passRate).toBeGreaterThan(0.85);
  });

  it('has low bias for identical variants', () => {
    const result = validateAA(100, 0.1, 500);
    expect(result.bias).toBeLessThan(0.1);
  });
});

describe('calculateSampleSize', () => {
  it('calculates reasonable sample sizes', () => {
    const n = calculateSampleSize(0.1, 0.05);
    expect(n).toBeGreaterThan(100);
    expect(n).toBeLessThan(100000);
  });

  it('requires more samples for smaller effect sizes', () => {
    const n1 = calculateSampleSize(0.1, 0.05);
    const n2 = calculateSampleSize(0.1, 0.02);
    expect(n2).toBeGreaterThan(n1);
  });

  it('requires more samples for lower baseline rates', () => {
    const n1 = calculateSampleSize(0.2, 0.05);
    const n2 = calculateSampleSize(0.05, 0.05);
    expect(n2).toBeGreaterThan(n1);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Assignment Strategy Tests
// ─────────────────────────────────────────────────────────────────────────────

describe('assignByStrategy', () => {
  const mockControl: VariantDoc = createMockVariant({
    id: 'var_control',
    isControl: true,
    stats: {
      participants: 100,
      events: 50,
      primaryMetricValue: 0.1,
      conversions: 10,
      conversionRate: 0.1,
      betaAlpha: 11,
      betaBeta: 91,
    },
  });

  const mockVariant: VariantDoc = createMockVariant({
    id: 'var_test',
    stats: {
      participants: 100,
      events: 50,
      primaryMetricValue: 0.2,
      conversions: 20,
      conversionRate: 0.2,
      betaAlpha: 21,
      betaBeta: 81,
    },
  });

  const ctx = {
    variants: [mockControl, mockVariant],
    controlVariant: mockControl,
    totalParticipants: 200,
  };

  it('returns valid variant for random strategy', () => {
    const variantId = assignByStrategy('random', ctx);
    expect([mockControl.id, mockVariant.id]).toContain(variantId);
  });

  it('returns valid variant for Thompson sampling', () => {
    const variantId = assignByStrategy('thompson', ctx);
    expect([mockControl.id, mockVariant.id]).toContain(variantId);
  });

  it('returns valid variant for epsilon-greedy', () => {
    const variantId = assignByStrategy('epsilon_greedy', { ...ctx, explorationRate: 0.1 });
    expect([mockControl.id, mockVariant.id]).toContain(variantId);
  });

  it('returns valid variant for UCB', () => {
    const variantId = assignByStrategy('ucb', ctx);
    expect([mockControl.id, mockVariant.id]).toContain(variantId);
  });
});
