import { describe, expect, it } from 'vitest';
import {
  applyCollectionTypeChange,
  applyPlaybackOrderChange,
  applyPlayoutModeChange,
  copyDraftItem,
  type DraftItem,
  durationFieldsEnabled,
  fillWithGroupModeEligible,
  fixedBehaviorEnabled,
  fromResponse,
  multipleCountEnabled,
  multipleModeEnabled,
  multipleModeOptions,
  newDraftItem,
  normalizeForSave,
  playbackOrderLabel,
  playbackOrderOptions,
  playbackOrderSelectDisabled,
  playoutModeOptions,
  type ScheduleItem,
  shufflePlaylistItemsVisible,
  startTimeEnabled,
  startTypeOptions,
  validateDraft,
  validateItem
} from './itemRules';

function draft(overrides: Partial<DraftItem> = {}): DraftItem {
  return { ...newDraftItem(), ...overrides };
}

describe('playbackOrderOptions', () => {
  it('MultiCollection offers Shuffle + ShuffleInOrder + WeightedShuffle only', () => {
    expect(playbackOrderOptions('MultiCollection')).toEqual(['Shuffle', 'ShuffleInOrder', 'WeightedShuffle']);
  });

  it('WeightedShuffle is offered ONLY on MultiCollection (needs per-source weights)', () => {
    const others: DraftItem['collectionType'][] = [
      'Collection',
      'SmartCollection',
      'SearchQuery',
      'TelevisionShow',
      'TelevisionSeason',
      'Artist',
      'Playlist',
      'RerunFirstRun',
      'RerunRerun'
    ];
    for (const t of others) {
      expect(playbackOrderOptions(t)).not.toContain('WeightedShuffle');
    }
  });

  it('Collection / SmartCollection / SearchQuery offer the collection-like set incl. Marathon', () => {
    const expected = ['Chronological', 'Random', 'Shuffle', 'ShuffleInOrder', 'Marathon'];
    expect(playbackOrderOptions('Collection')).toEqual(expected);
    expect(playbackOrderOptions('SmartCollection')).toEqual(expected);
    expect(playbackOrderOptions('SearchQuery')).toEqual(expected);
  });

  it('TelevisionShow offers Chronological/SeasonEpisode/Random/Shuffle/MultiEpisodeShuffle', () => {
    expect(playbackOrderOptions('TelevisionShow')).toEqual([
      'Chronological',
      'SeasonEpisode',
      'Random',
      'Shuffle',
      'MultiEpisodeShuffle'
    ]);
  });

  it('TelevisionSeason / Artist offer Chronological/Random/Shuffle', () => {
    expect(playbackOrderOptions('TelevisionSeason')).toEqual(['Chronological', 'Random', 'Shuffle']);
    expect(playbackOrderOptions('Artist')).toEqual(['Chronological', 'Random', 'Shuffle']);
  });

  it('Playlist / Rerun types offer only None', () => {
    expect(playbackOrderOptions('Playlist')).toEqual(['None']);
    expect(playbackOrderOptions('RerunFirstRun')).toEqual(['None']);
    expect(playbackOrderOptions('RerunRerun')).toEqual(['None']);
  });

  it('never offers RandomRotation', () => {
    const allTypes: DraftItem['collectionType'][] = [
      'Collection',
      'TelevisionShow',
      'TelevisionSeason',
      'Artist',
      'MultiCollection',
      'SmartCollection',
      'Playlist',
      'RerunFirstRun',
      'RerunRerun',
      'SearchQuery'
    ];
    for (const t of allTypes) {
      expect(playbackOrderOptions(t)).not.toContain('RandomRotation');
    }
  });
});

describe('playbackOrder selector disable + label', () => {
  it('disabled for Playlist and Rerun types only', () => {
    expect(playbackOrderSelectDisabled('Playlist')).toBe(true);
    expect(playbackOrderSelectDisabled('RerunFirstRun')).toBe(true);
    expect(playbackOrderSelectDisabled('RerunRerun')).toBe(true);
    expect(playbackOrderSelectDisabled('Collection')).toBe(false);
    expect(playbackOrderSelectDisabled('MultiCollection')).toBe(false);
  });

  it('labels None as "Playlist" for a Playlist type, plain otherwise', () => {
    expect(playbackOrderLabel('None', 'Playlist')).toBe('Playlist');
    expect(playbackOrderLabel('None', 'RerunFirstRun')).toBe('None');
    expect(playbackOrderLabel('MultiEpisodeShuffle', 'TelevisionShow')).toBe('Multi Episode Shuffle');
  });
});

describe('start + playout mode option lists gate on shuffleScheduleItems', () => {
  it('hides Fixed start type when the schedule shuffles items', () => {
    expect(startTypeOptions(false)).toEqual(['Dynamic', 'Fixed']);
    expect(startTypeOptions(true)).toEqual(['Dynamic']);
  });

  it('hides Flood playout mode when the schedule shuffles items', () => {
    expect(playoutModeOptions(false)).toEqual(['Flood', 'One', 'Multiple', 'Duration']);
    expect(playoutModeOptions(true)).toEqual(['One', 'Multiple', 'Duration']);
  });
});

describe('multipleModeOptions', () => {
  it('always offers Count; CollectionSize for non-Playlist, PlaylistItemSize for Playlist', () => {
    expect(multipleModeOptions('Collection', 'Shuffle')).toEqual(['Count', 'CollectionSize']);
    expect(multipleModeOptions('Playlist', 'None')).toEqual(['Count', 'PlaylistItemSize']);
  });

  it('adds MultiEpisodeGroupSize only for Chronological order', () => {
    expect(multipleModeOptions('Collection', 'Chronological')).toEqual([
      'Count',
      'CollectionSize',
      'MultiEpisodeGroupSize'
    ]);
    expect(multipleModeOptions('Collection', 'Random')).not.toContain('MultiEpisodeGroupSize');
  });
});

describe('enable gates', () => {
  it('start time + fixed behavior enabled only for Fixed', () => {
    expect(startTimeEnabled(draft({ startType: 'Fixed' }))).toBe(true);
    expect(startTimeEnabled(draft({ startType: 'Dynamic' }))).toBe(false);
    expect(fixedBehaviorEnabled(draft({ startType: 'Fixed' }))).toBe(true);
    expect(fixedBehaviorEnabled(draft({ startType: 'Dynamic' }))).toBe(false);
  });

  it('multipleMode enabled only for Multiple; multipleCount only for Multiple+Count', () => {
    expect(multipleModeEnabled(draft({ playoutMode: 'Multiple' }))).toBe(true);
    expect(multipleModeEnabled(draft({ playoutMode: 'One' }))).toBe(false);
    expect(multipleCountEnabled(draft({ playoutMode: 'Multiple', multipleMode: 'Count' }))).toBe(true);
    expect(multipleCountEnabled(draft({ playoutMode: 'Multiple', multipleMode: 'CollectionSize' }))).toBe(false);
    expect(multipleCountEnabled(draft({ playoutMode: 'Duration', multipleMode: 'Count' }))).toBe(false);
  });

  it('duration fields enabled only for Duration', () => {
    expect(durationFieldsEnabled(draft({ playoutMode: 'Duration' }))).toBe(true);
    expect(durationFieldsEnabled(draft({ playoutMode: 'Flood' }))).toBe(false);
  });

  it('shuffle-playlist-items checkbox visible only for Playlist type', () => {
    expect(shufflePlaylistItemsVisible(draft({ collectionType: 'Playlist' }))).toBe(true);
    expect(shufflePlaylistItemsVisible(draft({ collectionType: 'Collection' }))).toBe(false);
  });
});

describe('fillWithGroupModeEligible', () => {
  it('eligible for Multiple/Duration + non-ShuffleInOrder + Collection/Multi/Smart', () => {
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Multiple', playbackOrder: 'Shuffle', collectionType: 'Collection' }))).toBe(true);
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Duration', playbackOrder: 'Random', collectionType: 'MultiCollection' }))).toBe(true);
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Duration', playbackOrder: 'Random', collectionType: 'SmartCollection' }))).toBe(true);
  });

  it('ineligible for One/Flood, ShuffleInOrder, WeightedShuffle, or non-collection types', () => {
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'One', collectionType: 'Collection' }))).toBe(false);
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Flood', collectionType: 'Collection' }))).toBe(false);
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Multiple', playbackOrder: 'ShuffleInOrder', collectionType: 'Collection' }))).toBe(false);
    // WeightedShuffle joins ShuffleInOrder in the exclusion (#404): fillWithGroup schedules per-group,
    // which conflicts with WeightedShuffle's whole-multi-collection per-source airtime share.
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Multiple', playbackOrder: 'WeightedShuffle', collectionType: 'MultiCollection' }))).toBe(false);
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Multiple', playbackOrder: 'Shuffle', collectionType: 'TelevisionShow' }))).toBe(false);
    expect(fillWithGroupModeEligible(draft({ playoutMode: 'Multiple', playbackOrder: 'None', collectionType: 'Playlist' }))).toBe(false);
  });
});

describe('applyCollectionTypeChange', () => {
  it('nulls all source refs + names', () => {
    const before = draft({
      collectionType: 'Collection',
      collectionId: 5,
      collectionName: 'A',
      multiCollectionId: 6,
      multiCollectionName: 'B',
      smartCollectionId: 7,
      smartCollectionName: 'C',
      rerunCollectionId: 8,
      rerunCollectionName: 'D',
      mediaItemId: 9,
      mediaItemName: 'E',
      playlistId: 10,
      playlistName: 'F',
      playlistGroupId: 2,
      searchTitle: 'T',
      searchQuery: 'Q'
    });
    const after = applyCollectionTypeChange(before, 'TelevisionShow');
    expect(after.collectionType).toBe('TelevisionShow');
    for (const key of [
      'collectionId',
      'multiCollectionId',
      'smartCollectionId',
      'rerunCollectionId',
      'mediaItemId',
      'playlistId',
      'playlistGroupId',
      'searchTitle',
      'searchQuery',
      'collectionName',
      'multiCollectionName',
      'smartCollectionName',
      'rerunCollectionName',
      'playlistName',
      'mediaItemName'
    ] as const) {
      expect(after[key]).toBeNull();
    }
  });

  it('resets PlaylistItemSize -> Count when leaving Playlist', () => {
    const before = draft({ collectionType: 'Playlist', playbackOrder: 'None', multipleMode: 'PlaylistItemSize' });
    expect(applyCollectionTypeChange(before, 'Collection').multipleMode).toBe('Count');
  });

  it('resets CollectionSize -> Count when moving to Playlist (invalid there)', () => {
    // CollectionSize is not offered for a Playlist type (which offers PlaylistItemSize), so it must
    // snap to the first valid mode — otherwise the native <select> shows "Count" while state kept
    // "CollectionSize" (F2 repro).
    const before = draft({ collectionType: 'Collection', multipleMode: 'CollectionSize' });
    expect(applyCollectionTypeChange(before, 'Playlist').multipleMode).toBe('Count');
  });

  it('keeps PlaylistItemSize when moving to Playlist (valid there)', () => {
    const before = draft({ collectionType: 'Collection', multipleMode: 'PlaylistItemSize' });
    expect(applyCollectionTypeChange(before, 'Playlist').multipleMode).toBe('PlaylistItemSize');
  });

  it('F2: Collection+Marathon -> TelevisionShow drops Marathon (order + fields reconciled)', () => {
    // Repro: TelevisionShow does not offer Marathon; a stale playbackOrder:"Marathon" left the
    // select displaying "Chronological" while Marathon fields stayed visible.
    const before = draft({
      collectionType: 'Collection',
      playbackOrder: 'Marathon',
      marathonGroupBy: 'Season',
      marathonShuffleItems: true,
      marathonBatchSize: 4
    });
    const after = applyCollectionTypeChange(before, 'TelevisionShow');
    expect(playbackOrderOptions('TelevisionShow')).not.toContain('Marathon');
    expect(after.playbackOrder).toBe('Chronological'); // first TelevisionShow order
    expect(after.marathonGroupBy).toBe('None');
    expect(after.marathonShuffleItems).toBe(false);
    expect(after.marathonBatchSize).toBeNull();
  });

  it('F2 invariant: for every from/to type pair, playbackOrder and multipleMode land in the valid sets', () => {
    const allTypes: DraftItem['collectionType'][] = [
      'Collection',
      'TelevisionShow',
      'TelevisionSeason',
      'Artist',
      'MultiCollection',
      'SmartCollection',
      'Playlist',
      'RerunFirstRun',
      'RerunRerun',
      'SearchQuery'
    ];
    const seedModes: DraftItem['multipleMode'][] = ['Count', 'CollectionSize', 'PlaylistItemSize', 'MultiEpisodeGroupSize'];
    for (const from of allTypes) {
      for (const order of playbackOrderOptions(from)) {
        for (const mode of seedModes) {
          const before = draft({ collectionType: from, playbackOrder: order, multipleMode: mode });
          for (const to of allTypes) {
            const after = applyCollectionTypeChange(before, to);
            expect(playbackOrderOptions(to)).toContain(after.playbackOrder);
            expect(multipleModeOptions(to, after.playbackOrder)).toContain(after.multipleMode);
          }
        }
      }
    }
  });

  it('forces playbackOrder None for Playlist / Rerun types', () => {
    expect(applyCollectionTypeChange(draft(), 'Playlist').playbackOrder).toBe('None');
    expect(applyCollectionTypeChange(draft(), 'RerunFirstRun').playbackOrder).toBe('None');
    expect(applyCollectionTypeChange(draft(), 'RerunRerun').playbackOrder).toBe('None');
  });

  it('forces playbackOrder Shuffle for MultiCollection', () => {
    expect(applyCollectionTypeChange(draft({ playbackOrder: 'Chronological' }), 'MultiCollection').playbackOrder).toBe('Shuffle');
  });

  it('resets fillWithGroupMode when the new type makes it ineligible', () => {
    const before = draft({ playoutMode: 'Multiple', playbackOrder: 'Shuffle', collectionType: 'Collection', fillWithGroupMode: 'FillWithOrderedGroups' });
    expect(before.fillWithGroupMode).toBe('FillWithOrderedGroups');
    const after = applyCollectionTypeChange(before, 'TelevisionShow');
    expect(after.fillWithGroupMode).toBe('None');
  });
});

describe('applyPlaybackOrderChange', () => {
  it('resets MultiEpisodeGroupSize -> Count when not Chronological', () => {
    const before = draft({ playbackOrder: 'Chronological', multipleMode: 'MultiEpisodeGroupSize' });
    expect(applyPlaybackOrderChange(before, 'Random').multipleMode).toBe('Count');
    expect(applyPlaybackOrderChange(before, 'Chronological').multipleMode).toBe('MultiEpisodeGroupSize');
  });

  it('resets marathon fields (except shuffleGroups) when not Marathon', () => {
    const before = draft({
      playbackOrder: 'Marathon',
      marathonGroupBy: 'Season',
      marathonShuffleItems: true,
      marathonShuffleGroups: true,
      marathonBatchSize: 4
    });
    const after = applyPlaybackOrderChange(before, 'Shuffle');
    expect(after.marathonGroupBy).toBe('None');
    expect(after.marathonShuffleItems).toBe(false);
    expect(after.marathonBatchSize).toBeNull();
    expect(after.marathonShuffleGroups).toBe(true);
  });

  it('preserves marathon fields when staying Marathon', () => {
    const before = draft({ playbackOrder: 'Marathon', marathonGroupBy: 'Season', marathonBatchSize: 4 });
    const after = applyPlaybackOrderChange(before, 'Marathon');
    expect(after.marathonGroupBy).toBe('Season');
    expect(after.marathonBatchSize).toBe(4);
  });

  it('resets fillWithGroupMode to None for ShuffleInOrder', () => {
    const before = draft({ playoutMode: 'Multiple', collectionType: 'Collection', fillWithGroupMode: 'FillWithShuffledGroups' });
    expect(applyPlaybackOrderChange(before, 'ShuffleInOrder').fillWithGroupMode).toBe('None');
  });
});

describe('applyPlayoutModeChange', () => {
  it('resets fillWithGroupMode when moving to One/Flood', () => {
    const before = draft({ playoutMode: 'Multiple', playbackOrder: 'Shuffle', collectionType: 'Collection', fillWithGroupMode: 'FillWithOrderedGroups' });
    expect(applyPlayoutModeChange(before, 'One').fillWithGroupMode).toBe('None');
    expect(applyPlayoutModeChange(before, 'Duration').fillWithGroupMode).toBe('FillWithOrderedGroups');
  });
});

describe('factories', () => {
  it('newDraftItem uses the documented defaults', () => {
    const item = newDraftItem();
    expect(item).toMatchObject({
      startType: 'Dynamic',
      playoutMode: 'One',
      playbackOrder: 'Shuffle',
      collectionType: 'Collection',
      watermarkIds: [],
      graphicsElementIds: []
    });
    expect(item._key).toMatch(/^draft-/);
    // A never-persisted item must not carry a server id (#259) — the save projection relies on this
    // to tell the server it's an insert.
    expect(item.id).toBeNull();
  });

  it('copyDraftItem deep-copies multi/smart/rerun refs and gives a new key + array identity', () => {
    const original = draft({
      id: 99,
      multiCollectionId: 3,
      multiCollectionName: 'M',
      smartCollectionId: 4,
      smartCollectionName: 'S',
      rerunCollectionId: 5,
      rerunCollectionName: 'R',
      watermarkIds: [1, 2],
      graphicsElementIds: [9]
    });
    const copy = copyDraftItem(original);
    expect(copy.multiCollectionId).toBe(3);
    expect(copy.smartCollectionId).toBe(4);
    expect(copy.rerunCollectionId).toBe(5);
    expect(copy.multiCollectionName).toBe('M');
    expect(copy.rerunCollectionName).toBe('R');
    expect(copy._key).not.toBe(original._key);
    expect(copy.watermarkIds).toEqual([1, 2]);
    expect(copy.watermarkIds).not.toBe(original.watermarkIds);
    expect(copy.graphicsElementIds).not.toBe(original.graphicsElementIds);
    // A copy is a new row that was never persisted under its own id — it must not carry the source
    // item's server id (#259), or a save would submit the same id twice.
    expect(copy.id).toBeNull();
  });
});

describe('fromResponse', () => {
  it('maps watermarks/graphics named embeds to id arrays and copies mutation fields', () => {
    const model: ScheduleItem = {
      id: 42,
      index: 0,
      startType: 'Fixed',
      startTime: '06:00:00',
      fixedStartTimeBehavior: 'Strict',
      playoutMode: 'Duration',
      collectionType: 'SmartCollection',
      collectionId: null,
      multiCollectionId: null,
      smartCollectionId: 7,
      rerunCollectionId: null,
      mediaItemId: null,
      playlistId: null,
      searchTitle: null,
      searchQuery: null,
      playbackOrder: 'Shuffle',
      marathonGroupBy: 'None',
      marathonShuffleGroups: false,
      marathonShuffleItems: false,
      marathonBatchSize: null,
      fillWithGroupMode: 'FillWithOrderedGroups',
      multipleMode: null,
      multipleCount: null,
      playoutDuration: '01:00:00',
      tailMode: 'Offline',
      discardToFillAttempts: 2,
      customTitle: 'Nightly',
      guideMode: 'Normal',
      preRollFillerId: null,
      midRollFillerId: null,
      postRollFillerId: null,
      tailFillerId: null,
      fallbackFillerId: null,
      watermarkIds: [1, 2],
      graphicsElementIds: [3],
      preferredAudioLanguageCode: 'eng',
      preferredAudioTitle: null,
      preferredSubtitleLanguageCode: null,
      subtitleMode: null,
      collectionName: null,
      multiCollectionName: null,
      smartCollectionName: 'Smarts',
      rerunCollectionName: null,
      playlistName: null,
      playlistGroupId: null,
      mediaItemName: null,
      preRollFillerName: null,
      midRollFillerName: null,
      postRollFillerName: null,
      tailFillerName: null,
      fallbackFillerName: null,
      watermarks: [
        { id: 1, name: 'W1' },
        { id: 2, name: 'W2' }
      ],
      graphicsElements: [{ id: 3, name: 'G3' }],
      name: 'Smarts',
      durationEstimate: '01:00:00'
    };
    const item = fromResponse(model);
    // The server id must round-trip onto the draft (#259) so a subsequent save reconciles this row by
    // identity instead of by array position.
    expect(item.id).toBe(42);
    expect(item.smartCollectionId).toBe(7);
    expect(item.smartCollectionName).toBe('Smarts');
    expect(item.watermarkIds).toEqual([1, 2]);
    expect(item.graphicsElementIds).toEqual([3]);
    expect(item.multipleMode).toBe('Count'); // null coalesced
    expect(item.tailMode).toBe('Offline');
    expect(item.playoutDuration).toBe('01:00:00');
  });
});

describe('normalizeForSave', () => {
  it('nulls startTime + fixedStartTimeBehavior when not Fixed', () => {
    const req = normalizeForSave(draft({ startType: 'Dynamic', startTime: '06:00:00', fixedStartTimeBehavior: 'Strict' }));
    expect(req.startTime).toBeNull();
    expect(req.fixedStartTimeBehavior).toBeNull();
  });

  it('keeps startTime + behavior when Fixed', () => {
    const req = normalizeForSave(draft({ startType: 'Fixed', startTime: '06:00:00', fixedStartTimeBehavior: 'Flexible' }));
    expect(req.startTime).toBe('06:00:00');
    expect(req.fixedStartTimeBehavior).toBe('Flexible');
  });

  it('nulls multipleCount unless Multiple+Count', () => {
    expect(normalizeForSave(draft({ playoutMode: 'One', multipleCount: '5' })).multipleCount).toBeNull();
    expect(normalizeForSave(draft({ playoutMode: 'Multiple', multipleMode: 'CollectionSize', multipleCount: '5' })).multipleCount).toBeNull();
    expect(normalizeForSave(draft({ playoutMode: 'Multiple', multipleMode: 'Count', multipleCount: '5' })).multipleCount).toBe('5');
  });

  it('nulls duration fields (tailMode -> None) unless Duration', () => {
    const req = normalizeForSave(draft({ playoutMode: 'One', playoutDuration: '01:00:00', tailMode: 'Offline', discardToFillAttempts: 3 }));
    expect(req.playoutDuration).toBeNull();
    expect(req.tailMode).toBe('None');
    expect(req.discardToFillAttempts).toBeNull();
  });

  it('keeps duration fields when Duration', () => {
    const req = normalizeForSave(draft({ playoutMode: 'Duration', playoutDuration: '01:00:00', tailMode: 'Filler', discardToFillAttempts: 3 }));
    expect(req.playoutDuration).toBe('01:00:00');
    expect(req.tailMode).toBe('Filler');
    expect(req.discardToFillAttempts).toBe(3);
  });

  it('resets marathon fields (except shuffleGroups) unless Marathon', () => {
    const req = normalizeForSave(draft({ playbackOrder: 'Shuffle', marathonGroupBy: 'Season', marathonShuffleItems: true, marathonShuffleGroups: true, marathonBatchSize: 4 }));
    expect(req.marathonGroupBy).toBe('None');
    expect(req.marathonShuffleItems).toBe(false);
    expect(req.marathonBatchSize).toBeNull();
    expect(req.marathonShuffleGroups).toBe(true);
  });

  it('forces fillWithGroupMode None when ineligible', () => {
    const req = normalizeForSave(draft({ playoutMode: 'One', collectionType: 'Collection', fillWithGroupMode: 'FillWithOrderedGroups' }));
    expect(req.fillWithGroupMode).toBe('None');
  });

  it('does not emit local-only fields', () => {
    const req = normalizeForSave(draft());
    expect('_key' in req).toBe(false);
    expect('collectionName' in req).toBe(false);
    expect('name' in req).toBe(false);
  });

  it('passes the draft id through unchanged: null for a new item, the server id for a loaded one (#259)', () => {
    expect(normalizeForSave(draft()).id).toBeNull();
    expect(normalizeForSave(draft({ id: 42 })).id).toBe(42);
  });
});

describe('validation', () => {
  it('requires a start time for Fixed items', () => {
    expect(validateItem(draft({ startType: 'Fixed', startTime: null }))).toMatch(/start time/i);
    expect(validateItem(draft({ startType: 'Fixed', startTime: '06:00:00' }))).toBeNull();
  });

  it('requires a count expression for Multiple+Count', () => {
    expect(validateItem(draft({ playoutMode: 'Multiple', multipleMode: 'Count', multipleCount: null }))).toMatch(/count/i);
    expect(validateItem(draft({ playoutMode: 'Multiple', multipleMode: 'Count', multipleCount: '3' }))).toBeNull();
  });

  it('requires playoutDuration + non-negative discard for Duration', () => {
    expect(validateItem(draft({ playoutMode: 'Duration', playoutDuration: null }))).toMatch(/duration/i);
    expect(validateItem(draft({ playoutMode: 'Duration', playoutDuration: '01:00:00', discardToFillAttempts: null }))).toMatch(/discard/i);
    expect(validateItem(draft({ playoutMode: 'Duration', playoutDuration: '01:00:00', discardToFillAttempts: -1 }))).toMatch(/discard/i);
    expect(validateItem(draft({ playoutMode: 'Duration', playoutDuration: '01:00:00', discardToFillAttempts: 0 }))).toBeNull();
  });

  it('validateDraft prefixes the offending item index', () => {
    const items = [draft(), draft({ startType: 'Fixed', startTime: null })];
    expect(validateDraft(items)).toMatch(/^Item 2:/);
    expect(validateDraft([draft()])).toBeNull();
  });
});
