import { describe, expect, it } from 'vitest';

import {
  ProjectReducerError,
  projectReducer,
} from '../../../src/project/project.reducer';
import {
  selectCanExport,
  selectMissingAssets,
  selectTimelineDuration,
  selectTimelineEntries,
  selectUnusedAssets,
} from '../../../src/project/project.selectors';
import { createEmptyProject } from '../../../src/project/project.schema';
import {
  CREATED_AT,
  UPDATED_AT,
  mediaAsset,
  populatedProject,
} from './project-fixture';

describe('project reducer', () => {
  it('adds assets and sequential clips without mutating prior state', () => {
    const empty = createEmptyProject({
      id: 'p',
      timestamp: CREATED_AT,
    });
    const withAsset = projectReducer(empty, {
      type: 'asset/add',
      asset: mediaAsset(),
      updatedAt: UPDATED_AT,
    });
    const withClip = projectReducer(withAsset, {
      type: 'clip/add',
      clip: {
        id: 'clip-1',
        assetId: 'asset-1',
        sourceInSeconds: 0,
        sourceOutSeconds: 2,
      },
      updatedAt: UPDATED_AT,
    });

    expect(empty.assets).toEqual([]);
    expect(withAsset.timeline).toEqual([]);
    expect(withClip.timeline).toHaveLength(1);
    expect(withClip.updatedAt).toBe(UPDATED_AT);
  });

  it('reorders clips deterministically and returns identity for a no-op', () => {
    const project = {
      ...populatedProject(),
      timeline: [
        populatedProject().timeline[0]!,
        {
          id: 'clip-2',
          assetId: 'asset-1',
          sourceInSeconds: 4.5,
          sourceOutSeconds: 6,
        },
      ],
    };
    const moved = projectReducer(project, {
      type: 'clip/move',
      clipId: 'clip-2',
      toIndex: 0,
      updatedAt: UPDATED_AT,
    });
    expect(moved.timeline.map((clip) => clip.id)).toEqual(['clip-2', 'clip-1']);
    expect(
      projectReducer(moved, {
        type: 'clip/move',
        clipId: 'clip-2',
        toIndex: 0,
        updatedAt: UPDATED_AT,
      })
    ).toBe(moved);
  });

  it('removes dependent clips/subtitles and clears stale UI selection', () => {
    const project = {
      ...populatedProject(),
      subtitles: [
        { id: 'subtitle-1', assetId: 'asset-1', mode: 'mux' as const },
      ],
    };
    const result = projectReducer(project, {
      type: 'asset/remove',
      assetId: 'asset-1',
      updatedAt: UPDATED_AT,
    });

    expect(result.assets).toEqual([]);
    expect(result.timeline).toEqual([]);
    expect(result.subtitles).toEqual([]);
    expect(result.ui).toMatchObject({
      selectedAssetId: undefined,
      selectedClipId: undefined,
    });
  });

  it('guards duplicate IDs, missing assets and invalid ranges', () => {
    const project = populatedProject();
    expect(() =>
      projectReducer(project, {
        type: 'asset/add',
        asset: mediaAsset(),
        updatedAt: UPDATED_AT,
      })
    ).toThrow(ProjectReducerError);
    expect(() =>
      projectReducer(project, {
        type: 'clip/add',
        clip: {
          id: 'bad',
          assetId: 'missing',
          sourceInSeconds: 0,
          sourceOutSeconds: 1,
        },
        updatedAt: UPDATED_AT,
      })
    ).toThrow('unknown asset');
    expect(() =>
      projectReducer(project, {
        type: 'clip/update',
        clipId: 'clip-1',
        changes: { sourceOutSeconds: 0 },
        updatedAt: UPDATED_AT,
      })
    ).toThrow();
  });

  it('does not mark the document updated for transient UI state', () => {
    const project = populatedProject();
    const result = projectReducer(project, {
      type: 'ui/set',
      ui: { inspectorPanel: 'output' },
    });
    expect(result.updatedAt).toBe(project.updatedAt);
  });
});

describe('project selectors', () => {
  it('derives timeline positions and total duration for a sequential edit', () => {
    const project = {
      ...populatedProject(),
      timeline: [
        populatedProject().timeline[0]!,
        {
          id: 'clip-2',
          assetId: 'asset-1',
          sourceInSeconds: 5,
          sourceOutSeconds: 6.25,
        },
      ],
    };
    expect(selectTimelineDuration(project)).toBe(4.75);
    expect(selectTimelineEntries(project)).toMatchObject([
      {
        timelineStartSeconds: 0,
        timelineEndSeconds: 3.5,
        durationSeconds: 3.5,
      },
      {
        timelineStartSeconds: 3.5,
        timelineEndSeconds: 4.75,
        durationSeconds: 1.25,
      },
    ]);
  });

  it('identifies missing, unused and export-blocking sources', () => {
    const project = {
      ...populatedProject(),
      assets: [
        mediaAsset({ sourceStatus: 'missing' }),
        mediaAsset({ id: 'asset-unused', originalName: 'unused.wav' }),
      ],
    };
    expect(selectMissingAssets(project).map((asset) => asset.id)).toEqual([
      'asset-1',
    ]);
    expect(selectUnusedAssets(project).map((asset) => asset.id)).toEqual([
      'asset-unused',
    ]);
    expect(selectCanExport(project)).toEqual({
      canExport: false,
      reasons: ['1 referenced source file is missing.'],
    });
  });
});
