import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { zipSync } from 'fflate';
import { Cache } from '../src/services/cache.ts';
import { fetchEpub } from '../src/services/epub.ts';

const enc = new TextEncoder();

function buildEpub(chapters: { title: string; content: string }[]): Uint8Array {
  const files: Record<string, Uint8Array> = {};

  files['META-INF/container.xml'] = enc.encode(
    `<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>`,
  );

  const manifestItems = chapters
    .map((_, i) => `<item id="ch${i}" href="ch${i}.xhtml" media-type="application/xhtml+xml"/>`)
    .join('\n    ');
  const spineItems = chapters.map((_, i) => `<itemref idref="ch${i}"/>`).join('\n    ');

  files['content.opf'] = enc.encode(
    `<?xml version="1.0"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
  <manifest>
    ${manifestItems}
  </manifest>
  <spine>
    ${spineItems}
  </spine>
</package>`,
  );

  for (let i = 0; i < chapters.length; i++) {
    const ch = chapters[i]!;
    files[`ch${i}.xhtml`] = enc.encode(
      `<html><body><div>${ch.content}</div></body></html>`,
    );
  }

  return zipSync(files);
}

let tmpDir: string;
let cache: Cache;

beforeEach(() => {
  tmpDir = mkdtempSync(join(tmpdir(), 'qa-epub-test-'));
  cache = new Cache(tmpDir);
});

afterEach(() => {
  rmSync(tmpDir, { recursive: true, force: true });
});

function mockFetch(data: Uint8Array) {
  return spyOn(globalThis, 'fetch').mockResolvedValue(
    new Response(data, { status: 200 }),
  );
}

describe('epub — parsing', () => {
  test('extracts the correct number of chapters', async () => {
    const epub = buildEpub([
      { title: 'Ch 1', content: 'First chapter content here.' },
      { title: 'Ch 2', content: 'Second chapter content here.' },
      { title: 'Ch 3', content: 'Third chapter content here.' },
    ]);
    const spy = mockFetch(epub);

    let rangeStart = -1, rangeEnd = -1;
    const view = await fetchEpub('http://fake/book.epub', 1, 0, 2, cache, (s, e) => {
      rangeStart = s; rangeEnd = e;
    });

    expect(view.chapters).toHaveLength(3);
    expect(rangeStart).toBe(0);
    expect(rangeEnd).toBe(2);
    spy.mockRestore();
  });

  test('serves from cache on second call without re-fetching', async () => {
    const epub = buildEpub([
      { title: 'Ch 1', content: 'Chapter one long enough content.' },
    ]);
    const spy = mockFetch(epub);

    await fetchEpub('http://fake/book.epub', 2, 0, 0, cache, () => {});
    expect(spy).toHaveBeenCalledTimes(1);

    // Second call — should hit cache
    await fetchEpub('http://fake/book.epub', 2, 0, 0, cache, () => {});
    expect(spy).toHaveBeenCalledTimes(1);

    spy.mockRestore();
  });

  test('skips blank chapters', async () => {
    const epub = buildEpub([
      { title: 'Cover', content: '   ' },       // blank — should be skipped
      { title: 'Ch 1', content: 'Real content here for this chapter.' },
      { title: 'Ch 2', content: 'More real content for the next chapter.' },
    ]);
    const spy = mockFetch(epub);

    let rangeEnd = -1;
    await fetchEpub('http://fake/book.epub', 3, 0, 10, cache, (_, e) => { rangeEnd = e; });
    expect(rangeEnd).toBe(1); // only 2 non-blank chapters (indices 0 and 1)
    spy.mockRestore();
  });

  test('throws on HTTP error', async () => {
    const spy = spyOn(globalThis, 'fetch').mockResolvedValue(
      new Response('Not Found', { status: 404 }),
    );
    await expect(fetchEpub('http://fake/missing.epub', 4, 0, 0, cache, () => {})).rejects.toThrow(
      'HTTP 404',
    );
    spy.mockRestore();
  });

  test('slices to the requested range', async () => {
    const epub = buildEpub([
      { title: 'Ch 0', content: 'Chapter zero has enough content here.' },
      { title: 'Ch 1', content: 'Chapter one has enough content here.' },
      { title: 'Ch 2', content: 'Chapter two has enough content here.' },
      { title: 'Ch 3', content: 'Chapter three has enough content here.' },
    ]);
    const spy = mockFetch(epub);

    const view = await fetchEpub('http://fake/book.epub', 5, 1, 2, cache, () => {});
    expect(view.min).toBe(1);
    expect(view.max).toBe(2);
    expect(view.chapters).toHaveLength(2);
    spy.mockRestore();
  });
});
