import { fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';

import { OneNoteApplication } from './App.js';
import type {
  OneNotePackageDto,
  OneNoteSectionDto,
} from './onenote/model/dto.js';
import {
  MAX_SOURCE_FILE_BYTES,
  sourceKindFromFileName,
} from './source-file.js';
import {
  OneNoteWorkerClient,
  type WorkerPort,
} from './worker/onenote.client.js';
import type {
  WorkerRequest,
  WorkerResponse,
} from './worker/worker-protocol.js';

const section: OneNoteSectionDto = {
  format: 'one',
  sourceName: 'Section.one',
  sectionName: 'Project notes',
  pages: [
    {
      id: 'page-1',
      title: 'First meeting',
      text: 'This is the complete extracted page text.',
      textPreview: 'This is the complete extracted page text.',
      createdAt: '2024-01-02T10:00:00.000Z',
    },
  ],
  diagnostics: [
    {
      severity: 'warning',
      code: 'UNSUPPORTED_IMAGE',
      message: 'An image was not rendered.',
      recoverable: true,
    },
  ],
  parserInfo: {
    implementation: 'typescript',
    supportedVariant: 'desktop-revision-store',
    referenceRevision: 'test',
  },
};

class SuccessfulParserWorker implements WorkerPort {
  onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null = null;
  onerror: ((event: ErrorEvent) => void) | null = null;
  terminated = false;

  constructor(private readonly packageResult?: OneNotePackageDto) {}

  postMessage(message: WorkerRequest): void {
    queueMicrotask(() => {
      if (message.type === 'parse-one') {
        this.respond({
          type: 'section',
          requestId: message.requestId,
          sessionId: 'session-one',
          section,
        });
      } else if (message.type === 'parse-onepkg' && this.packageResult) {
        this.respond({
          type: 'package',
          requestId: message.requestId,
          sessionId: 'session-package',
          notebook: this.packageResult,
        });
      } else if (message.type === 'get-page') {
        const page = section.pages[0]!;
        this.respond({
          type: 'page',
          requestId: message.requestId,
          sessionId: message.sessionId,
          page: {
            ...page,
            id: message.pageId,
            blocks: [
              {
                kind: 'text',
                id: 'text-1',
                sourceText: page.text,
                text: page.text,
                runs: [
                  {
                    start: 0,
                    end: page.text.length,
                    text: page.text,
                    style: {},
                  },
                ],
                paragraphStyle: {},
                layout: {},
              },
            ],
            diagnostics: [],
          },
        });
      }
    });
  }

  terminate(): void {
    this.terminated = true;
  }

  private respond(response: WorkerResponse): void {
    this.onmessage?.(new MessageEvent('message', { data: response }));
  }
}

class FailingParserWorker implements WorkerPort {
  onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null = null;
  onerror: ((event: ErrorEvent) => void) | null = null;

  postMessage(message: WorkerRequest): void {
    if (message.type !== 'parse-one' && message.type !== 'parse-onepkg') return;
    queueMicrotask(() => {
      this.onmessage?.(
        new MessageEvent('message', {
          data: {
            type: 'failure',
            requestId: message.requestId,
            error: {
              code: 'invalid-format',
              message: 'The source header is not a recognized OneNote file.',
              recoverable: false,
              structure: 'OneStore header',
            },
          } satisfies WorkerResponse,
        })
      );
    });
  }

  terminate(): void {}
}

describe('OneNoteApplication', () => {
  it('recognizes only supported local filename extensions', () => {
    expect(sourceKindFromFileName('Section.ONE')).toBe('one');
    expect(sourceKindFromFileName('Notebook.OnePkg')).toBe('onepkg');
    expect(sourceKindFromFileName('notes.zip')).toBeNull();
  });

  it('opens a section, navigates its page, shows diagnostics, and clears', async () => {
    const user = userEvent.setup();
    const worker = new SuccessfulParserWorker();
    render(
      <OneNoteApplication
        createWorkerClient={() => new OneNoteWorkerClient(() => worker)}
      />
    );

    await user.upload(
      screen.getByLabelText('Choose a OneNote file'),
      new File([new Uint8Array([1, 2, 3])], 'Section.one')
    );

    expect(
      await screen.findByRole('heading', { name: 'First meeting' })
    ).toBeInTheDocument();
    expect(
      within(screen.getByRole('article', { name: 'First meeting' })).getByText(
        'This is the complete extracted page text.'
      )
    ).toBeInTheDocument();
    expect(screen.getByText('UNSUPPORTED_IMAGE')).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: 'Clear file' }));
    expect(worker.terminated).toBe(true);
    expect(
      screen.getByRole('heading', { name: 'Open a OneNote file' })
    ).toBeInTheDocument();
  });

  it('opens a dropped OneNote package through the local worker', async () => {
    const notebook: OneNotePackageDto = {
      format: 'onepkg',
      sourceName: 'Notebook.onepkg',
      sourceSize: 1,
      entries: [],
      sections: [],
      tree: { interpreted: false, nodes: [] },
      diagnostics: [],
    };
    render(
      <OneNoteApplication
        createWorkerClient={() =>
          new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook))
        }
      />
    );

    fireEvent.drop(screen.getByTestId('file-drop-zone'), {
      dataTransfer: {
        files: [new File([new Uint8Array([1])], 'Notebook.onepkg')],
      },
    });

    expect(
      await screen.findByRole('heading', { name: 'Package summary' })
    ).toBeInTheDocument();
  });

  it('keeps a failed packaged section visible beside a readable one', async () => {
    const notebook: OneNotePackageDto = {
      format: 'onepkg',
      sourceName: 'Notebook.onepkg',
      sourceSize: 4096,
      entries: [
        {
          path: 'Notes/Readable.one',
          normalizedPath: 'Notes/Readable.one',
          kind: 'section',
          uncompressedSize: 2048,
          compressionMethod: 'LZX',
          parseStatus: 'parsed',
        },
        {
          path: 'Notes/Broken.one',
          normalizedPath: 'Notes/Broken.one',
          kind: 'section',
          uncompressedSize: 1024,
          compressionMethod: 'LZX',
          parseStatus: 'failed',
        },
      ],
      sections: [
        {
          id: 'readable',
          path: 'Notes/Readable.one',
          displayName: 'Readable',
          parseStatus: 'parsed',
          section,
          diagnostics: [],
        },
        {
          id: 'broken',
          path: 'Notes/Broken.one',
          displayName: 'Broken',
          parseStatus: 'failed',
          diagnostics: [
            {
              severity: 'error',
              code: 'SECTION_PARSE_FAILED',
              message: 'This section is malformed.',
              recoverable: true,
            },
          ],
        },
      ],
      tree: {
        interpreted: true,
        tocPath: 'Open Notebook.onetoc2',
        nodes: [
          {
            kind: 'section-group',
            id: 'Notes',
            displayName: 'Notes',
            children: [
              {
                kind: 'section',
                id: 'readable',
                sectionId: 'readable',
                displayName: 'Readable',
              },
              {
                kind: 'section',
                id: 'broken',
                sectionId: 'broken',
                displayName: 'Broken',
              },
            ],
          },
        ],
      },
      diagnostics: [
        {
          severity: 'warning',
          code: 'TEST_PACKAGE_WARNING',
          message: 'Example recoverable package warning.',
          recoverable: true,
        },
      ],
    };
    const user = userEvent.setup();
    render(
      <OneNoteApplication
        createWorkerClient={() =>
          new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook))
        }
      />
    );

    await user.upload(
      screen.getByLabelText('Choose a OneNote file'),
      new File([new Uint8Array([1])], 'Notebook.onepkg')
    );

    expect(
      await screen.findByRole('heading', { name: 'Package summary' })
    ).toBeInTheDocument();
    expect(screen.getByText('Notes')).toBeVisible();
    expect(screen.getByText('OneNote notebook order')).toBeVisible();
    expect(
      screen.getByRole('button', { name: /Broken, failed/i })
    ).toBeVisible();
    expect(screen.getByText('TEST_PACKAGE_WARNING')).toBeInTheDocument();
    expect(
      await within(
        screen.getByRole('article', { name: 'First meeting' })
      ).findByText('This is the complete extracted page text.')
    ).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /Broken, failed/i }));
    expect(screen.getByText('SECTION_PARSE_FAILED')).toBeInTheDocument();
    expect(
      screen.getByText('This section did not produce a readable page list.')
    ).toBeInTheDocument();
  });

  it('shows a controlled parser failure', async () => {
    const user = userEvent.setup();
    render(
      <OneNoteApplication
        createWorkerClient={() =>
          new OneNoteWorkerClient(() => new FailingParserWorker())
        }
      />
    );

    await user.upload(
      screen.getByLabelText('Choose a OneNote file'),
      new File(['not OneNote'], 'Broken.one')
    );

    expect(await screen.findByRole('alert')).toHaveTextContent(
      'The source header is not a recognized OneNote file.'
    );
    expect(screen.getByRole('alert')).toHaveTextContent(
      'invalid-format · OneStore header'
    );
  });

  it('rejects unrelated extensions without starting a worker', async () => {
    const user = userEvent.setup({ applyAccept: false });
    render(<OneNoteApplication />);

    await user.upload(
      screen.getByLabelText('Choose a OneNote file'),
      new File(['not a notebook'], 'notes.zip')
    );

    expect(await screen.findByRole('alert')).toHaveTextContent(
      'Choose a file whose name ends in .one or .onepkg.'
    );
  });

  it('enforces the source-size safety limit before reading', async () => {
    const user = userEvent.setup();
    render(<OneNoteApplication />);
    const file = new File(['tiny'], 'large.one');
    Object.defineProperty(file, 'size', {
      configurable: true,
      value: MAX_SOURCE_FILE_BYTES + 1,
    });

    await user.upload(screen.getByLabelText('Choose a OneNote file'), file);

    expect(await screen.findByRole('alert')).toHaveTextContent('512 MiB');
  });
});
