/**
 * Comprehensive tests for the production-grade feature flag system.
 *
 * Covers: schema validation, evaluator engine (targeting rules, segments,
 * prerequisites, scheduling, gradual rollouts, individual targets),
 * version comparison, and deterministic hashing.
 */

import { describe, it, expect } from 'vitest';
import {
  CreateFlagSchema,
  UpdateFlagSchema,
  CreateSegmentSchema,
  EvaluateSchema,
  type FeatureFlagDoc,
  type SegmentDoc,
} from './types.js';
import {
  evaluateFlag,
  evaluateAllFlags,
  hashUserFlag,
  compareVersions,
  getEffectivePercentage,
} from './evaluator.js';

// ── Test helpers ────────────────────────────────────────────────────────────

function makeFlag(overrides: Partial<FeatureFlagDoc> = {}): FeatureFlagDoc {
  return {
    id: 'flag_test_feature',
    productId: 'test',
    key: 'feature',
    flagType: 'boolean',
    enabled: true,
    archived: false,
    description: 'Test flag',
    tags: [],
    variations: [
      { key: 'on', value: true, description: 'On' },
      { key: 'off', value: false, description: 'Off' },
    ],
    defaultVariation: 'on',
    offVariation: 'off',
    platforms: [],
    regions: [],
    osVersions: [],
    segments: [],
    targetingRules: [],
    individualTargets: {},
    prerequisites: [],
    percentage: 100,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
    version: 1,
    ...overrides,
  };
}

function makeSegment(overrides: Partial<SegmentDoc> = {}): SegmentDoc {
  return {
    id: 'seg_test_beta',
    productId: 'test',
    key: 'beta',
    name: 'Beta Users',
    description: 'Beta testers',
    rules: [],
    includedUsers: [],
    excludedUsers: [],
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
    ...overrides,
  };
}

// ── Schema validation ───────────────────────────────────────────────────────

describe('CreateFlagSchema', () => {
  it('accepts valid flag with defaults', () => {
    const result = CreateFlagSchema.safeParse({ key: 'dark_mode' });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.enabled).toBe(true);
      expect(result.data.flagType).toBe('boolean');
      expect(result.data.percentage).toBe(100);
      expect(result.data.platforms).toEqual([]);
      expect(result.data.targetingRules).toEqual([]);
      expect(result.data.individualTargets).toEqual({});
      expect(result.data.prerequisites).toEqual([]);
    }
  });

  it('accepts dotted key names', () => {
    const result = CreateFlagSchema.safeParse({ key: 'smartauth.oauth.google' });
    expect(result.success).toBe(true);
  });

  it('accepts multi-variate string flag', () => {
    const result = CreateFlagSchema.safeParse({
      key: 'cta_color',
      flagType: 'string',
      variations: [
        { key: 'red', value: '#FF0000' },
        { key: 'blue', value: '#0000FF' },
        { key: 'green', value: '#00FF00' },
      ],
      defaultVariation: 'red',
      offVariation: 'blue',
    });
    expect(result.success).toBe(true);
  });

  it('accepts targeting rules', () => {
    const result = CreateFlagSchema.safeParse({
      key: 'new_ui',
      targetingRules: [
        {
          id: 'rule_1',
          clauses: [{ attribute: 'email', operator: 'ends_with', values: ['@bytelyst.com'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    expect(result.success).toBe(true);
  });

  it('accepts schedule with gradual rollout', () => {
    const result = CreateFlagSchema.safeParse({
      key: 'gradual',
      schedule: {
        enableAt: '2026-04-01T00:00:00Z',
        gradualRollout: {
          startPercentage: 5,
          endPercentage: 100,
          startAt: '2026-04-01T00:00:00Z',
          endAt: '2026-04-08T00:00:00Z',
        },
      },
    });
    expect(result.success).toBe(true);
  });

  it('accepts prerequisites', () => {
    const result = CreateFlagSchema.safeParse({
      key: 'child_flag',
      prerequisites: [{ flagKey: 'parent_flag', variationKey: 'on' }],
    });
    expect(result.success).toBe(true);
  });

  it('rejects key with spaces', () => {
    expect(CreateFlagSchema.safeParse({ key: 'dark mode' }).success).toBe(false);
  });

  it('rejects key with uppercase', () => {
    expect(CreateFlagSchema.safeParse({ key: 'DarkMode' }).success).toBe(false);
  });

  it('rejects percentage > 100', () => {
    expect(CreateFlagSchema.safeParse({ key: 'test', percentage: 150 }).success).toBe(false);
  });

  it('rejects percentage < 0', () => {
    expect(CreateFlagSchema.safeParse({ key: 'test', percentage: -10 }).success).toBe(false);
  });

  it('rejects targeting rule with empty clauses', () => {
    const result = CreateFlagSchema.safeParse({
      key: 'bad',
      targetingRules: [{ id: 'r1', clauses: [], variationKey: 'on' }],
    });
    expect(result.success).toBe(false);
  });
});

describe('UpdateFlagSchema', () => {
  it('accepts partial updates', () => {
    expect(UpdateFlagSchema.safeParse({ enabled: false, percentage: 25 }).success).toBe(true);
  });

  it('accepts empty object', () => {
    expect(UpdateFlagSchema.safeParse({}).success).toBe(true);
  });

  it('accepts archived field', () => {
    expect(UpdateFlagSchema.safeParse({ archived: true }).success).toBe(true);
  });

  it('accepts targeting rules update', () => {
    expect(
      UpdateFlagSchema.safeParse({
        targetingRules: [
          {
            id: 'rule_1',
            clauses: [{ attribute: 'platform', operator: 'eq', values: ['ios'] }],
            variationKey: 'on',
          },
        ],
      }).success
    ).toBe(true);
  });
});

describe('CreateSegmentSchema', () => {
  it('accepts valid segment', () => {
    const result = CreateSegmentSchema.safeParse({
      key: 'beta_users',
      name: 'Beta Users',
      rules: [{ attribute: 'email', operator: 'ends_with', values: ['@bytelyst.com'] }],
      includedUsers: ['user_1'],
    });
    expect(result.success).toBe(true);
  });

  it('rejects empty name', () => {
    expect(CreateSegmentSchema.safeParse({ key: 'bad', name: '' }).success).toBe(false);
  });
});

describe('EvaluateSchema', () => {
  it('accepts full context', () => {
    const result = EvaluateSchema.safeParse({
      userId: 'user_1',
      platform: 'ios',
      region: 'us',
      osVersion: '17.2',
      appVersion: '2.1.0',
      email: 'test@bytelyst.com',
      custom: { plan: 'pro', age: 25 },
    });
    expect(result.success).toBe(true);
  });

  it('accepts empty context', () => {
    expect(EvaluateSchema.safeParse({}).success).toBe(true);
  });
});

// ── Evaluator engine ────────────────────────────────────────────────────────

describe('evaluateFlag', () => {
  it('returns off when flag is disabled', () => {
    const flag = makeFlag({ enabled: false });
    const result = evaluateFlag({ flag, ctx: {}, allFlags: [flag], segments: [] });
    expect(result.reason).toBe('off');
    expect(result.value).toBe(false);
    expect(result.variationKey).toBe('off');
  });

  it('returns off when flag is archived', () => {
    const flag = makeFlag({ archived: true });
    const result = evaluateFlag({ flag, ctx: {}, allFlags: [flag], segments: [] });
    expect(result.reason).toBe('off');
  });

  it('returns default variation when fully enabled (100%)', () => {
    const flag = makeFlag();
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'user_1' },
      allFlags: [flag],
      segments: [],
    });
    expect(result.reason).toBe('percentage_rollout');
    expect(result.value).toBe(true);
    expect(result.variationKey).toBe('on');
  });

  it('returns off when percentage is 0', () => {
    const flag = makeFlag({ percentage: 0 });
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'user_1' },
      allFlags: [flag],
      segments: [],
    });
    expect(result.reason).toBe('off');
    expect(result.value).toBe(false);
  });

  it('returns default with no userId at 100%', () => {
    const flag = makeFlag();
    const result = evaluateFlag({ flag, ctx: {}, allFlags: [flag], segments: [] });
    // At 100%, the evaluator takes the percentage_rollout fast path
    expect(result.reason).toBe('percentage_rollout');
    expect(result.value).toBe(true);
  });

  // ── Platform / region / OS version filtering ──────────────────────────

  it('filters by platform', () => {
    const flag = makeFlag({ platforms: ['ios'] });
    const on = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios' },
      allFlags: [flag],
      segments: [],
    });
    expect(on.reason).not.toBe('off');
    const off = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'android' },
      allFlags: [flag],
      segments: [],
    });
    expect(off.reason).toBe('off');
  });

  it('filters by region', () => {
    const flag = makeFlag({ regions: ['us', 'eu'] });
    const on = evaluateFlag({
      flag,
      ctx: { userId: 'u1', region: 'us' },
      allFlags: [flag],
      segments: [],
    });
    expect(on.reason).not.toBe('off');
    const off = evaluateFlag({
      flag,
      ctx: { userId: 'u1', region: 'apac' },
      allFlags: [flag],
      segments: [],
    });
    expect(off.reason).toBe('off');
  });

  it('filters by OS version range', () => {
    const flag = makeFlag({
      osVersions: [{ platform: 'ios', minVersion: '17.0', maxVersion: '18.0' }],
    });
    const on = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios', osVersion: '17.2' },
      allFlags: [flag],
      segments: [],
    });
    expect(on.reason).not.toBe('off');
    const off = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios', osVersion: '16.4' },
      allFlags: [flag],
      segments: [],
    });
    expect(off.reason).toBe('off');
  });

  // ── Individual targeting ──────────────────────────────────────────────

  it('matches individual user target', () => {
    const flag = makeFlag({
      individualTargets: { user_vip: 'on', user_blocked: 'off' },
    });
    const vip = evaluateFlag({ flag, ctx: { userId: 'user_vip' }, allFlags: [flag], segments: [] });
    expect(vip.reason).toBe('individual_target');
    expect(vip.value).toBe(true);

    const blocked = evaluateFlag({
      flag,
      ctx: { userId: 'user_blocked' },
      allFlags: [flag],
      segments: [],
    });
    expect(blocked.reason).toBe('individual_target');
    expect(blocked.value).toBe(false);
  });

  // ── Targeting rules ───────────────────────────────────────────────────

  it('evaluates targeting rule with eq operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'platform', operator: 'eq', values: ['ios'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const ios = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios' },
      allFlags: [flag],
      segments: [],
    });
    expect(ios.reason).toBe('rule_match');
    expect(ios.value).toBe(true);

    const android = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'android' },
      allFlags: [flag],
      segments: [],
    });
    expect(android.reason).toBe('off');
  });

  it('evaluates targeting rule with ends_with operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'email', operator: 'ends_with', values: ['@bytelyst.com'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const match = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'dev@bytelyst.com' },
      allFlags: [flag],
      segments: [],
    });
    expect(match.reason).toBe('rule_match');

    const noMatch = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'user@gmail.com' },
      allFlags: [flag],
      segments: [],
    });
    expect(noMatch.reason).toBe('off');
  });

  it('evaluates targeting rule with in operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'region', operator: 'in', values: ['us', 'eu'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const us = evaluateFlag({
      flag,
      ctx: { userId: 'u1', region: 'us' },
      allFlags: [flag],
      segments: [],
    });
    expect(us.reason).toBe('rule_match');
    const apac = evaluateFlag({
      flag,
      ctx: { userId: 'u1', region: 'apac' },
      allFlags: [flag],
      segments: [],
    });
    expect(apac.reason).toBe('off');
  });

  it('evaluates targeting rule with regex operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'email', operator: 'regex', values: ['^admin@'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const match = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'admin@bytelyst.com' },
      allFlags: [flag],
      segments: [],
    });
    expect(match.reason).toBe('rule_match');
    const noMatch = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'user@bytelyst.com' },
      allFlags: [flag],
      segments: [],
    });
    expect(noMatch.reason).toBe('off');
  });

  it('evaluates targeting rule with semver_gte operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'appVersion', operator: 'semver_gte', values: ['2.0.0'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const v2 = evaluateFlag({
      flag,
      ctx: { userId: 'u1', appVersion: '2.1.0' },
      allFlags: [flag],
      segments: [],
    });
    expect(v2.reason).toBe('rule_match');
    const v1 = evaluateFlag({
      flag,
      ctx: { userId: 'u1', appVersion: '1.9.0' },
      allFlags: [flag],
      segments: [],
    });
    expect(v1.reason).toBe('off');
  });

  it('evaluates custom attribute in targeting rule', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'plan', operator: 'eq', values: ['pro'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const pro = evaluateFlag({
      flag,
      ctx: { userId: 'u1', custom: { plan: 'pro' } },
      allFlags: [flag],
      segments: [],
    });
    expect(pro.reason).toBe('rule_match');
    const free = evaluateFlag({
      flag,
      ctx: { userId: 'u1', custom: { plan: 'free' } },
      allFlags: [flag],
      segments: [],
    });
    expect(free.reason).toBe('off');
  });

  it('requires all clauses to match in a rule (AND logic)', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [
            { attribute: 'platform', operator: 'eq', values: ['ios'] },
            { attribute: 'region', operator: 'eq', values: ['us'] },
          ],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const both = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios', region: 'us' },
      allFlags: [flag],
      segments: [],
    });
    expect(both.reason).toBe('rule_match');
    const onlyPlatform = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios', region: 'eu' },
      allFlags: [flag],
      segments: [],
    });
    expect(onlyPlatform.reason).toBe('off');
  });

  it('first matching rule wins', () => {
    const flag = makeFlag({
      key: 'multi_rule',
      percentage: 0,
      variations: [
        { key: 'on', value: true },
        { key: 'off', value: false },
        { key: 'beta', value: 'beta-ui' },
      ],
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'email', operator: 'ends_with', values: ['@bytelyst.com'] }],
          variationKey: 'beta',
          rolloutPercentage: 100,
        },
        {
          id: 'r2',
          clauses: [{ attribute: 'platform', operator: 'eq', values: ['ios'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    // Matches both rules — first (beta) should win
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'dev@bytelyst.com', platform: 'ios' },
      allFlags: [flag],
      segments: [],
    });
    expect(result.variationKey).toBe('beta');
    expect(result.value).toBe('beta-ui');
  });

  // ── Segments ──────────────────────────────────────────────────────────

  it('matches user in segment includedUsers', () => {
    const flag = makeFlag({ percentage: 0, segments: ['beta'] });
    const segment = makeSegment({ includedUsers: ['user_1'] });

    const match = evaluateFlag({
      flag,
      ctx: { userId: 'user_1' },
      allFlags: [flag],
      segments: [segment],
    });
    expect(match.reason).toBe('segment_match');
    expect(match.value).toBe(true);
  });

  it('excludes user in segment excludedUsers', () => {
    const flag = makeFlag({ percentage: 0, segments: ['beta'] });
    const segment = makeSegment({ includedUsers: ['user_1'], excludedUsers: ['user_1'] });

    const result = evaluateFlag({
      flag,
      ctx: { userId: 'user_1' },
      allFlags: [flag],
      segments: [segment],
    });
    expect(result.reason).toBe('off');
  });

  it('matches segment via rules', () => {
    const flag = makeFlag({ percentage: 0, segments: ['internal'] });
    const segment = makeSegment({
      key: 'internal',
      rules: [{ attribute: 'email', operator: 'ends_with', values: ['@bytelyst.com'] }],
    });

    const match = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'dev@bytelyst.com' },
      allFlags: [flag],
      segments: [segment],
    });
    expect(match.reason).toBe('segment_match');
  });

  // ── Prerequisites ─────────────────────────────────────────────────────

  it('fails when prerequisite flag is off', () => {
    const parent = makeFlag({ key: 'parent', enabled: false });
    const child = makeFlag({
      key: 'child',
      prerequisites: [{ flagKey: 'parent', variationKey: 'on' }],
    });
    const result = evaluateFlag({
      flag: child,
      ctx: { userId: 'u1' },
      allFlags: [parent, child],
      segments: [],
    });
    expect(result.reason).toBe('prerequisite_failed');
  });

  it('succeeds when prerequisite flag is on with correct variation', () => {
    const parent = makeFlag({ key: 'parent', enabled: true });
    const child = makeFlag({
      key: 'child',
      prerequisites: [{ flagKey: 'parent', variationKey: 'on' }],
    });
    const result = evaluateFlag({
      flag: child,
      ctx: { userId: 'u1' },
      allFlags: [parent, child],
      segments: [],
    });
    expect(result.reason).not.toBe('prerequisite_failed');
  });

  it('returns off for anonymous user with partial percentage', () => {
    const flag = makeFlag({ percentage: 50 });
    const result = evaluateFlag({ flag, ctx: {}, allFlags: [flag], segments: [] });
    expect(result.reason).toBe('off');
    expect(result.value).toBe(false);
  });

  it('applies partial rollout on targeting rule', () => {
    // Rule has 0% rollout — even matching users don't get it
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'platform', operator: 'eq', values: ['ios'] }],
          variationKey: 'on',
          rolloutPercentage: 0,
        },
      ],
    });
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios' },
      allFlags: [flag],
      segments: [],
    });
    expect(result.reason).toBe('off');
  });

  it('evaluates neq operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'platform', operator: 'neq', values: ['web'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const ios = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'ios' },
      allFlags: [flag],
      segments: [],
    });
    expect(ios.reason).toBe('rule_match');
    const web = evaluateFlag({
      flag,
      ctx: { userId: 'u1', platform: 'web' },
      allFlags: [flag],
      segments: [],
    });
    expect(web.reason).toBe('off');
  });

  it('evaluates contains operator', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'email', operator: 'contains', values: ['bytelyst'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const match = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'dev@bytelyst.com' },
      allFlags: [flag],
      segments: [],
    });
    expect(match.reason).toBe('rule_match');
    const noMatch = evaluateFlag({
      flag,
      ctx: { userId: 'u1', email: 'dev@gmail.com' },
      allFlags: [flag],
      segments: [],
    });
    expect(noMatch.reason).toBe('off');
  });

  it('evaluates gt and lt operators on numeric custom attribute', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [
            { attribute: 'age', operator: 'gte', values: [18] },
            { attribute: 'age', operator: 'lt', values: [65] },
          ],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const adult = evaluateFlag({
      flag,
      ctx: { userId: 'u1', custom: { age: 30 } },
      allFlags: [flag],
      segments: [],
    });
    expect(adult.reason).toBe('rule_match');
    const minor = evaluateFlag({
      flag,
      ctx: { userId: 'u1', custom: { age: 15 } },
      allFlags: [flag],
      segments: [],
    });
    expect(minor.reason).toBe('off');
  });

  it('returns off when clause attribute is missing from context', () => {
    const flag = makeFlag({
      percentage: 0,
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'email', operator: 'eq', values: ['test@test.com'] }],
          variationKey: 'on',
          rolloutPercentage: 100,
        },
      ],
    });
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'u1' },
      allFlags: [flag],
      segments: [],
    });
    expect(result.reason).toBe('off');
  });

  it('handles circular prerequisites gracefully', () => {
    const a = makeFlag({ key: 'a', prerequisites: [{ flagKey: 'b', variationKey: 'on' }] });
    const b = makeFlag({ key: 'b', prerequisites: [{ flagKey: 'a', variationKey: 'on' }] });
    const result = evaluateFlag({ flag: a, ctx: { userId: 'u1' }, allFlags: [a, b], segments: [] });
    expect(result.reason).toBe('error');
  });

  it('fails when prerequisite flag does not exist', () => {
    const child = makeFlag({
      key: 'child',
      prerequisites: [{ flagKey: 'nonexistent', variationKey: 'on' }],
    });
    const result = evaluateFlag({
      flag: child,
      ctx: { userId: 'u1' },
      allFlags: [child],
      segments: [],
    });
    expect(result.reason).toBe('prerequisite_failed');
  });

  // ── Scheduling ────────────────────────────────────────────────────────

  it('returns schedule_inactive before enableAt', () => {
    const flag = makeFlag({
      schedule: { enableAt: '2026-06-01T00:00:00Z' },
    });
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'u1' },
      allFlags: [flag],
      segments: [],
      now: '2026-05-15T00:00:00Z',
    });
    expect(result.reason).toBe('schedule_inactive');
  });

  it('returns active after enableAt', () => {
    const flag = makeFlag({
      schedule: { enableAt: '2026-06-01T00:00:00Z' },
    });
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'u1' },
      allFlags: [flag],
      segments: [],
      now: '2026-06-02T00:00:00Z',
    });
    expect(result.reason).not.toBe('schedule_inactive');
  });

  it('returns schedule_inactive after disableAt', () => {
    const flag = makeFlag({
      schedule: { disableAt: '2026-06-01T00:00:00Z' },
    });
    const result = evaluateFlag({
      flag,
      ctx: { userId: 'u1' },
      allFlags: [flag],
      segments: [],
      now: '2026-06-02T00:00:00Z',
    });
    expect(result.reason).toBe('schedule_inactive');
  });
});

// ── Gradual rollout ─────────────────────────────────────────────────────────

describe('getEffectivePercentage', () => {
  it('returns flag percentage when no gradual rollout', () => {
    const flag = makeFlag({ percentage: 50 });
    expect(getEffectivePercentage(flag, '2026-01-01T00:00:00Z')).toBe(50);
  });

  it('returns startPercentage before rollout starts', () => {
    const flag = makeFlag({
      schedule: {
        gradualRollout: {
          startPercentage: 5,
          endPercentage: 100,
          startAt: '2026-04-01T00:00:00Z',
          endAt: '2026-04-08T00:00:00Z',
        },
      },
    });
    expect(getEffectivePercentage(flag, '2026-03-31T00:00:00Z')).toBe(5);
  });

  it('returns endPercentage after rollout ends', () => {
    const flag = makeFlag({
      schedule: {
        gradualRollout: {
          startPercentage: 5,
          endPercentage: 100,
          startAt: '2026-04-01T00:00:00Z',
          endAt: '2026-04-08T00:00:00Z',
        },
      },
    });
    expect(getEffectivePercentage(flag, '2026-04-09T00:00:00Z')).toBe(100);
  });

  it('interpolates at midpoint', () => {
    const flag = makeFlag({
      schedule: {
        gradualRollout: {
          startPercentage: 0,
          endPercentage: 100,
          startAt: '2026-04-01T00:00:00Z',
          endAt: '2026-04-11T00:00:00Z', // 10 days
        },
      },
    });
    const pct = getEffectivePercentage(flag, '2026-04-06T00:00:00Z'); // 5 days in
    expect(pct).toBe(50);
  });
});

// ── Multi-variate evaluation ────────────────────────────────────────────────

describe('multi-variate flags', () => {
  it('returns string variation value', () => {
    const flag = makeFlag({
      key: 'theme',
      flagType: 'string',
      variations: [
        { key: 'dark', value: 'dark-theme' },
        { key: 'light', value: 'light-theme' },
      ],
      defaultVariation: 'dark',
      offVariation: 'light',
    });
    const result = evaluateFlag({ flag, ctx: { userId: 'u1' }, allFlags: [flag], segments: [] });
    expect(result.value).toBe('dark-theme');
    expect(result.variationKey).toBe('dark');
  });

  it('returns JSON variation via targeting rule', () => {
    const flag = makeFlag({
      key: 'config',
      flagType: 'json',
      percentage: 0,
      variations: [
        { key: 'v1', value: { maxItems: 10 } },
        { key: 'v2', value: { maxItems: 50 } },
      ],
      defaultVariation: 'v1',
      offVariation: 'v1',
      targetingRules: [
        {
          id: 'r1',
          clauses: [{ attribute: 'plan', operator: 'eq', values: ['pro'] }],
          variationKey: 'v2',
          rolloutPercentage: 100,
        },
      ],
    });
    const pro = evaluateFlag({
      flag,
      ctx: { userId: 'u1', custom: { plan: 'pro' } },
      allFlags: [flag],
      segments: [],
    });
    expect(pro.value).toEqual({ maxItems: 50 });
    expect(pro.variationKey).toBe('v2');
  });
});

// ── evaluateAllFlags ────────────────────────────────────────────────────────

describe('evaluateAllFlags', () => {
  it('evaluates all non-archived flags', () => {
    const flags = [
      makeFlag({ key: 'a', enabled: true }),
      makeFlag({ key: 'b', enabled: false }),
      makeFlag({ key: 'c', archived: true }),
    ];
    const results = evaluateAllFlags(flags, { userId: 'u1' }, []);
    expect(Object.keys(results)).toEqual(['a', 'b']);
    expect(results['a'].reason).not.toBe('off');
    expect(results['b'].reason).toBe('off');
  });
});

// ── Version comparison ──────────────────────────────────────────────────────

describe('compareVersions', () => {
  it('equal versions return 0', () => {
    expect(compareVersions('17.2.1', '17.2.1')).toBe(0);
  });

  it('a < b returns -1', () => {
    expect(compareVersions('16.4', '17.0')).toBe(-1);
    expect(compareVersions('17.0.0', '17.0.1')).toBe(-1);
  });

  it('a > b returns 1', () => {
    expect(compareVersions('18.0', '17.2.1')).toBe(1);
    expect(compareVersions('17.1', '17.0.9')).toBe(1);
  });

  it('handles different segment lengths', () => {
    expect(compareVersions('17.0', '17.0.0')).toBe(0);
    expect(compareVersions('17', '17.0.0')).toBe(0);
    expect(compareVersions('17.0.1', '17')).toBe(1);
  });

  it('handles single-segment versions', () => {
    expect(compareVersions('14', '15')).toBe(-1);
    expect(compareVersions('15', '14')).toBe(1);
  });
});

// ── Deterministic hashing ───────────────────────────────────────────────────

describe('hashUserFlag', () => {
  it('returns the same bucket for the same user+flag', () => {
    const a = hashUserFlag('user_123', 'dark_mode');
    const b = hashUserFlag('user_123', 'dark_mode');
    expect(a).toBe(b);
  });

  it('returns different buckets for different users', () => {
    const buckets = new Set<number>();
    for (let i = 0; i < 20; i++) {
      buckets.add(hashUserFlag(`user_${i}`, 'feature_x'));
    }
    expect(buckets.size).toBeGreaterThanOrEqual(5);
  });

  it('returns different buckets for different flags', () => {
    const a = hashUserFlag('user_42', 'flag_a');
    const b = hashUserFlag('user_42', 'flag_b');
    expect(a).not.toBe(b);
  });

  it('always returns 0-99', () => {
    for (let i = 0; i < 100; i++) {
      const bucket = hashUserFlag(`user_${i}`, `flag_${i}`);
      expect(bucket).toBeGreaterThanOrEqual(0);
      expect(bucket).toBeLessThan(100);
    }
  });

  it('produces roughly uniform distribution', () => {
    let below50 = 0;
    const total = 1000;
    for (let i = 0; i < total; i++) {
      if (hashUserFlag(`user_${i}`, 'ab_test') < 50) below50++;
    }
    const ratio = below50 / total;
    expect(ratio).toBeGreaterThan(0.3);
    expect(ratio).toBeLessThan(0.7);
  });
});
