import { describe, it, expect } from 'vitest';
import { Brief, BriefDecisions, BriefStatus, LibraryPassage } from './brief';
import { RichTextBlock } from '@shared/kernel/rich-text';
import { PlaceholderDef } from './placeholders';
import { BriefState, BriefMsg, reduce } from './brief.machine';

const placeholders: PlaceholderDef[] = [
  { key: 'naam', label: 'Naam', autoResolvable: true },
  { key: 'reden', label: 'Reden', autoResolvable: false },
];

const text = (t: string): RichTextBlock => ({
  paragraphs: [{ nodes: [{ type: 'text', text: t }] }],
});

const libPassage = (id: string, sectionKey: string): LibraryPassage => ({
  passageId: id,
  scope: 'global',
  sectionKey,
  label: `Passage ${id}`,
  content: text(`inhoud ${id}`),
  version: 3,
});

function briefWith(status: BriefStatus, sections?: Brief['sections']): Brief {
  return {
    briefId: 'b1',
    beroep: 'arts',
    templateId: 't1',
    placeholders,
    sections: sections ?? [
      { sectionKey: 'aanhef', title: 'Aanhef', required: true, locked: false, blocks: [] },
      { sectionKey: 'slot', title: 'Slot', required: false, locked: false, blocks: [] },
    ],
    status,
    drafterId: 'u1',
  };
}

// A machine test cares about status transitions, not who may act — a fixed,
// unrestrictive fixture keeps every existing assertion focused on that.
const decisions: BriefDecisions = {
  canEdit: true,
  canApprove: true,
  canReject: true,
  canSend: true,
};

const loaded = (
  status: BriefStatus = { tag: 'draft' },
  sections?: Brief['sections'],
): BriefState => ({
  tag: 'loaded',
  brief: briefWith(status, sections),
  availablePassages: [libPassage('p1', 'aanhef'), libPassage('p2', 'aanhef')],
  decisions,
});

const sectionBlocks = (s: BriefState, key: string) =>
  s.tag === 'loaded' ? s.brief.sections.find((x) => x.sectionKey === key)!.blocks : [];

describe('brief.machine reduce', () => {
  it('BriefLoaded / BriefLoadFailed / Seed set state directly', () => {
    expect(
      reduce(initialLoading(), {
        tag: 'BriefLoaded',
        brief: briefWith({ tag: 'draft' }),
        availablePassages: [],
        decisions,
      }).tag,
    ).toBe('loaded');
    expect(reduce(initialLoading(), { tag: 'BriefLoadFailed', reason: 'x' })).toEqual({
      tag: 'failed',
      reason: 'x',
    });
    const seeded = loaded();
    expect(reduce(initialLoading(), { tag: 'Seed', state: seeded })).toBe(seeded);
  });

  it('PassagesInserted creates one frozen block per passage, in order, with local ids', () => {
    const s = reduce(loaded(), {
      tag: 'PassagesInserted',
      sectionKey: 'aanhef',
      passages: [libPassage('p1', 'aanhef'), libPassage('p2', 'aanhef')],
    });
    const blocks = sectionBlocks(s, 'aanhef');
    expect(blocks.map((b) => b.blockId)).toEqual(['local-1', 'local-2']);
    expect(blocks.every((b) => b.type === 'passage' && b.edited === false)).toBe(true);
    expect(blocks[0].type === 'passage' && blocks[0].sourcePassageId).toBe('p1');
  });

  it('PassagesInserted deep-copies content — later library mutation does not leak in', () => {
    const passage = libPassage('p1', 'aanhef');
    const s = reduce(loaded(), {
      tag: 'PassagesInserted',
      sectionKey: 'aanhef',
      passages: [passage],
    });
    // Mutate the source passage object after insertion.
    (passage.content.paragraphs[0].nodes as { type: 'text'; text: string }[])[0].text = 'HACKED';
    const block = sectionBlocks(s, 'aanhef')[0];
    expect(block.content.paragraphs[0].nodes[0]).toEqual({ type: 'text', text: 'inhoud p1' });
  });

  it('FreeTextBlockAdded appends an empty free-text block', () => {
    const s = reduce(loaded(), { tag: 'FreeTextBlockAdded', sectionKey: 'slot' });
    const blocks = sectionBlocks(s, 'slot');
    expect(blocks).toHaveLength(1);
    expect(blocks[0].type).toBe('freeText');
  });

  it('BlockContentEdited replaces content and marks a passage block edited', () => {
    let s = reduce(loaded(), {
      tag: 'PassagesInserted',
      sectionKey: 'aanhef',
      passages: [libPassage('p1', 'aanhef')],
    });
    s = reduce(s, { tag: 'BlockContentEdited', blockId: 'local-1', content: text('aangepast') });
    const block = sectionBlocks(s, 'aanhef')[0];
    expect(block.type === 'passage' && block.edited).toBe(true);
    expect(block.content).toEqual(text('aangepast'));
  });

  it('BlockRemoved and BlockMovedWithinSection reorder within a section', () => {
    let s = reduce(loaded(), {
      tag: 'PassagesInserted',
      sectionKey: 'aanhef',
      passages: [libPassage('p1', 'aanhef'), libPassage('p2', 'aanhef')],
    });
    s = reduce(s, { tag: 'BlockMovedWithinSection', blockId: 'local-1', toIndex: 1 });
    expect(sectionBlocks(s, 'aanhef').map((b) => b.blockId)).toEqual(['local-2', 'local-1']);
    s = reduce(s, { tag: 'BlockRemoved', blockId: 'local-2' });
    expect(sectionBlocks(s, 'aanhef').map((b) => b.blockId)).toEqual(['local-1']);
  });

  it('edits to a locked section are no-ops (insert, free-text, content, remove, move)', () => {
    const lockedSections: Brief['sections'] = [
      {
        sectionKey: 'aanhef',
        title: 'Aanhef',
        required: true,
        locked: true,
        blocks: [{ type: 'freeText', blockId: 'local-1', content: text('vast') }],
      },
      { sectionKey: 'kern', title: 'Kern', required: true, locked: false, blocks: [] },
    ];
    const s = loaded({ tag: 'draft' }, lockedSections);
    // The brief value is left untouched (withEdit reallocates state, but the guard returns
    // the same brief), so assert on deep equality of the section contents.
    expect(
      reduce(s, {
        tag: 'PassagesInserted',
        sectionKey: 'aanhef',
        passages: [libPassage('p1', 'aanhef')],
      }),
    ).toEqual(s);
    expect(reduce(s, { tag: 'FreeTextBlockAdded', sectionKey: 'aanhef' })).toEqual(s);
    expect(
      reduce(s, { tag: 'BlockContentEdited', blockId: 'local-1', content: text('gehackt') }),
    ).toEqual(s);
    expect(reduce(s, { tag: 'BlockRemoved', blockId: 'local-1' })).toEqual(s);
    expect(reduce(s, { tag: 'BlockMovedWithinSection', blockId: 'local-1', toIndex: 0 })).toEqual(
      s,
    );
    // the unlocked section still accepts edits
    const edited = reduce(s, { tag: 'FreeTextBlockAdded', sectionKey: 'kern' });
    expect(sectionBlocks(edited, 'kern')).toHaveLength(1);
  });

  it('edits are no-ops once submitted (status invariant)', () => {
    const s = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' });
    expect(reduce(s, { tag: 'FreeTextBlockAdded', sectionKey: 'slot' })).toBe(s);
  });

  it('editing a rejected letter reopens it to draft', () => {
    const s = loaded({
      tag: 'rejected',
      rejectedBy: 'u2',
      rejectedAt: 't',
      comments: 'graag aanpassen',
    });
    const next = reduce(s, { tag: 'FreeTextBlockAdded', sectionKey: 'slot' });
    expect(next.tag === 'loaded' && next.brief.status.tag).toBe('draft');
    expect(sectionBlocks(next, 'slot')).toHaveLength(1);
  });

  it('Submitted fires only from draft and only when required sections are filled', () => {
    // required 'aanhef' empty → no-op
    expect(reduce(loaded(), { tag: 'Submitted', by: 'u1', at: 't', decisions })).toEqual(loaded());
    // fill the required section, then submit
    const filled = reduce(loaded(), { tag: 'FreeTextBlockAdded', sectionKey: 'aanhef' });
    const submitted = reduce(filled, { tag: 'Submitted', by: 'u1', at: 't', decisions });
    expect(submitted.tag === 'loaded' && submitted.brief.status).toEqual({
      tag: 'submitted',
      submittedBy: 'u1',
      submittedAt: 't',
    });
  });

  it('approve/reject fire only from submitted; send only from approved', () => {
    const submitted = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' });
    // approve from draft is a no-op
    expect(reduce(loaded(), { tag: 'Approved', by: 'u2', at: 't', decisions })).toEqual(loaded());
    const approved = reduce(submitted, { tag: 'Approved', by: 'u2', at: 't2', decisions });
    expect(approved.tag === 'loaded' && approved.brief.status).toEqual({
      tag: 'approved',
      approvedBy: 'u2',
      approvedAt: 't2',
    });
    // reject carries comments
    const rejected = reduce(submitted, {
      tag: 'Rejected',
      by: 'u2',
      at: 't2',
      comments: 'nee',
      decisions,
    });
    expect(rejected.tag === 'loaded' && rejected.brief.status).toEqual({
      tag: 'rejected',
      rejectedBy: 'u2',
      rejectedAt: 't2',
      comments: 'nee',
    });
    // send only from approved
    expect(reduce(submitted, { tag: 'Sent', at: 't', decisions })).toBe(submitted);
    const sent = reduce(approved, { tag: 'Sent', at: 't3', decisions });
    expect(sent.tag === 'loaded' && sent.brief.status).toEqual({ tag: 'sent', sentAt: 't3' });
  });

  it('a status transition replaces decisions with the fresh server value', () => {
    const submitted = loaded({ tag: 'submitted', submittedBy: 'u1', submittedAt: 't' });
    const staleApprover: BriefDecisions = {
      canEdit: false,
      canApprove: false,
      canReject: false,
      canSend: false,
    };
    const approved = reduce(submitted, {
      tag: 'Approved',
      by: 'u2',
      at: 't2',
      decisions: staleApprover,
    });
    expect(approved.tag === 'loaded' && approved.decisions).toEqual(staleApprover);
  });
});

function initialLoading(): BriefState {
  return { tag: 'loading' };
}
