/**
 * Book content generation — depth, structure and continuity.
 *
 * Guards the fix for books that generated as short summaries: the prompt's
 * word targets now come from the book's own depth configuration, a multi-chapter
 * book is written chapter by chapter rather than in one thin pass, each chapter
 * receives what the previous one covered, and a chapter that lands far below
 * target is expanded once instead of being saved as the finished text.
 */

import fs from 'fs';
import os from 'os';
import path from 'path';

// ---- Mocks -------------------------------------------------------------
const mockGenerateWithAI = jest.fn();
const mockBookChapter = { findByIdAndUpdate: jest.fn().mockResolvedValue(null) };
const mockBookSection = { findByIdAndUpdate: jest.fn().mockResolvedValue(null) };
const mockBook = { findById: jest.fn().mockResolvedValue(null) };
const mockEmptyModel = { findOne: jest.fn().mockResolvedValue(null), find: jest.fn().mockResolvedValue([]) };

jest.mock('../../models', () => ({
  getModels: () => ({
    Book: mockBook,
    BookChapter: mockBookChapter,
    BookSection: mockBookSection,
    BusinessProfile: mockEmptyModel,
    ModuleData: mockEmptyModel,
  }),
}));
jest.mock('../../middleware/auth', () => ({ authenticate: (_r: any, _s: any, n: any) => n() }));
jest.mock('../../middleware/permissions', () => ({ requirePermission: () => (_r: any, _s: any, n: any) => n() }));
jest.mock('../../utils/aiProvider', () => ({ generateWithAI: (...a: any[]) => mockGenerateWithAI(...a) }));
jest.mock('../../services/aiContext/aiJobManager', () => ({
  createJob: () => ({ jobId: 'j1' }),
  updateJobProgress: jest.fn(),
  completeJob: jest.fn(),
  failJob: jest.fn(),
  getJob: () => null,
}));
// Heavy render libs are irrelevant to generation — stub so the module loads fast.
jest.mock('pdfkit', () => class {});
jest.mock('docx', () => ({}));
jest.mock('../../services/books/bookTypesetting', () => ({
  parseGeneratedBookHtml: jest.fn(),
  renderBookPdf: jest.fn(),
  renderBookDocx: jest.fn(),
}));

import { generateBookContentCore } from '../bookGenerator';

// ---- Fixtures ----------------------------------------------------------

const BOOK = {
  id: 'book-1',
  _id: 'book-1',
  title: 'Practical Systems Design',
  description: 'How to design systems that survive contact with production.',
  targetAudience: 'Engineers moving into architecture roles',
};

function makeChapters(count: number) {
  return Array.from({ length: count }, (_, i) => ({
    id: `ch-${i + 1}`,
    _id: `ch-${i + 1}`,
    title: `Chapter ${i + 1} Topic`,
    description: `What chapter ${i + 1} covers`,
    order: i,
  }));
}

function makeSections(chapters: any[], perChapter: number) {
  const out: Record<string, any[]> = {};
  chapters.forEach((ch) => {
    out[ch.id] = Array.from({ length: perChapter }, (_, j) => ({
      id: `${ch.id}-sec-${j + 1}`,
      _id: `${ch.id}-sec-${j + 1}`,
      title: `Section ${j + 1} of ${ch.title}`,
      type: 'content',
      order: j,
    }));
  });
  return out;
}

const CONFIG = (overrides: Record<string, any> = {}) => ({
  bookType: 'guide',
  writingStyle: 'professional',
  toneOfVoice: 'informative',
  contentDepth: 'intermediate',
  imagesEnabled: false,
  imageStyle: '',
  imageAspect: '',
  outputFormat: 'html',
  ...overrides,
}) as any;

/** A chapter response of roughly `words` words, matching the expected schema. */
function chapterResponse(chapterId: string, sectionIds: string[], words: number) {
  const filler = (n: number) => `<p>${'substantive sentence about the topic. '.repeat(Math.max(1, Math.round(n / 6)))}</p>`;
  return JSON.stringify({
    id: chapterId,
    title: 'Generated',
    content: `<h3>Opening</h3>${filler(Math.round(words * 0.3))}`,
    sections: sectionIds.map((sid) => ({
      id: sid,
      title: 'Generated section',
      content: `<h3>Sub</h3>${filler(Math.round((words * 0.7) / Math.max(sectionIds.length, 1)))}`,
      keyPoints: ['a point'],
      examples: ['an example'],
    })),
  });
}

/** Every prompt string handed to the AI during the run. */
const promptsSent = () => mockGenerateWithAI.mock.calls.map((c) => String(c[0]));

/**
 * Which chapter a prompt is asking for. Read from the JSON schema block rather
 * than the first `ch-N` in the text — a prompt also quotes the previous
 * chapter's content, so a naive match returns the wrong chapter.
 */
function chapterIdFromPrompt(prompt: string): string {
  const fromSchema = prompt.match(/"id":\s*"(ch-\d+)"/);
  if (fromSchema) return fromSchema[1];
  return (prompt.match(/ch-\d+/) || ['ch-1'])[0];
}

let tmpCwd: string;
let originalCwd: string;

beforeAll(() => {
  // The generator writes uploads/books/<id>/ — keep that out of the repo.
  originalCwd = process.cwd();
  tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'book-gen-test-'));
  process.chdir(tmpCwd);
});

afterAll(() => {
  process.chdir(originalCwd);
  try { fs.rmSync(tmpCwd, { recursive: true, force: true }); } catch { /* best effort */ }
});

beforeEach(() => {
  jest.clearAllMocks();
  mockBook.findById.mockResolvedValue(null); // status updates are a no-op here
});

describe('Book generation depth', () => {
  it('writes a multi-chapter book chapter by chapter instead of one thin pass', async () => {
    const chapters = makeChapters(4);
    const sections = makeSections(chapters, 3);
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      const chapterId = chapterIdFromPrompt(prompt);
      return Promise.resolve({
        content: chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 3500),
      });
    });

    const result = await generateBookContentCore(
      BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-1'
    );

    // One call per chapter — no whole-book single pass.
    expect(mockGenerateWithAI).toHaveBeenCalledTimes(4);
    expect(result.chapters).toHaveLength(4);
    // Every chapter present exactly once, none empty, no duplicates.
    expect(new Set(result.chapters.map((c: any) => c.id)).size).toBe(4);
    result.chapters.forEach((c: any) => expect(c.content.length).toBeGreaterThan(500));
    // Every section came back written.
    Object.values(result.sections).forEach((secs: any) => {
      expect(secs).toHaveLength(3);
      secs.forEach((s: any) => expect(s.content).not.toMatch(/placeholder content/i));
    });
  });

  it('states depth-derived word targets and drops the old short floors', async () => {
    const chapters = makeChapters(2);
    const sections = makeSections(chapters, 2);
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      const chapterId = chapterIdFromPrompt(prompt);
      return Promise.resolve({
        content: chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 4000),
      });
    });

    await generateBookContentCore(BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-2');

    const prompt = promptsSent()[0];
    // The floors that produced summary-length chapters are gone.
    expect(prompt).not.toMatch(/minimum 500-1000 words/i);
    expect(prompt).not.toMatch(/MUST have 300\+ words/i);
    expect(prompt).not.toMatch(/minimum 300-500 words/i);
    // Replaced by explicit targets and a structure contract.
    expect(prompt).toMatch(/CONTENT DEPTH REQUIREMENTS/);
    expect(prompt).toMatch(/~1,?100 words/);
    expect(prompt).toMatch(/what it is, why it matters, how it works/i);
    expect(prompt).toMatch(/Invent no statistics, studies, citations/i);
    // Audience travels with the request.
    expect(prompt).toContain('Engineers moving into architecture roles');
  });

  it('scales the targets with the configured depth', async () => {
    const chapters = makeChapters(2);
    const sections = makeSections(chapters, 2);
    const targetsFor = async (contentDepth: string) => {
      jest.clearAllMocks();
      mockGenerateWithAI.mockImplementation((prompt: string) => {
        const chapterId = chapterIdFromPrompt(prompt);
        return Promise.resolve({
          content: chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 8000),
        });
      });
      await generateBookContentCore(
        BOOK, chapters, sections, CONFIG({ contentDepth }), 'c1', () => {}, 'job-3'
      );
      const m = promptsSent()[0].match(/Whole chapter including its \d+ sections?: \*\*~([\d,]+) words/);
      return Number((m?.[1] || '0').replace(/,/g, ''));
    };

    const beginner = await targetsFor('beginner');
    const intermediate = await targetsFor('intermediate');
    const expert = await targetsFor('expert');

    expect(beginner).toBeGreaterThan(1500);
    expect(intermediate).toBeGreaterThan(beginner);
    expect(expert).toBeGreaterThan(intermediate);
  });

  it("honours the book's own target word count over the depth default", async () => {
    const chapters = makeChapters(4);
    const sections = makeSections(chapters, 2);
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      const chapterId = chapterIdFromPrompt(prompt);
      return Promise.resolve({
        content: chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 9000),
      });
    });

    await generateBookContentCore(
      { ...BOOK, wordCount: 32000 }, chapters, sections, CONFIG(), 'c1', () => {}, 'job-4'
    );

    // 32,000 words over 4 chapters → ~8,000 per chapter.
    expect(promptsSent()[0]).toMatch(/~8,000 words/);
  });

  it('gives each chapter what the previous one covered, and what comes next', async () => {
    const chapters = makeChapters(3);
    const sections = makeSections(chapters, 2);
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      const chapterId = chapterIdFromPrompt(prompt);
      const body = JSON.parse(chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 3500));
      body.content = `<h3>Distinctive heading for ${chapterId}</h3>${body.content}`;
      return Promise.resolve({ content: JSON.stringify(body) });
    });

    await generateBookContentCore(BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-5');

    const [first, second, third] = promptsSent();
    // The opening chapter has no predecessor to carry.
    expect(first).not.toMatch(/What it already covered/);
    expect(first).toContain('**Next chapter:** "Chapter 2 Topic"');
    // Later chapters receive the real digest of the chapter before them.
    expect(second).toMatch(/What it already covered/);
    expect(second).toContain('Distinctive heading for ch-1');
    expect(second).toContain('do NOT re-explain it');
    expect(third).toContain('Distinctive heading for ch-2');
    // And the closing chapter knows it is the close.
    expect(third).toMatch(/closing chapter/);
  });

  it('expands a chapter that comes back far below target, once', async () => {
    // Four chapters, so the run goes chapter by chapter and the first AI call
    // is chapter 1 rather than a whole-book single pass.
    const chapters = makeChapters(4);
    const sections = makeSections(chapters, 2);
    const thin = (id: string) => chapterResponse(id, sections[id].map((s: any) => s.id), 200);
    const full = (id: string) => chapterResponse(id, sections[id].map((s: any) => s.id), 4000);

    let call = 0;
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      call++;
      const chapterId = chapterIdFromPrompt(prompt);
      // First chapter answers thin, then answers in full when asked to expand.
      if (call === 1) return Promise.resolve({ content: thin(chapterId) });
      return Promise.resolve({ content: full(chapterId) });
    });

    const result = await generateBookContentCore(
      BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-6'
    );

    // chapter 1 + its expansion + chapters 2-4
    expect(mockGenerateWithAI).toHaveBeenCalledTimes(5);
    expect(promptsSent()[1]).toMatch(/REVISION REQUIRED/);
    expect(promptsSent()[1]).toMatch(/Add the missing development/);
    // The expanded draft is what gets saved.
    expect(result.chapters[0].content.length).toBeGreaterThan(
      JSON.parse(thin('ch-1')).content.length
    );
  });

  it('keeps a short book on the single-pass fast path only when it comes back at depth', async () => {
    const chapters = makeChapters(2);
    const sections = makeSections(chapters, 2);
    mockGenerateWithAI.mockImplementation(() => Promise.resolve({
      content: JSON.stringify({
        chapters: chapters.map((ch) => ({
          ...JSON.parse(chapterResponse(ch.id, sections[ch.id].map((s: any) => s.id), 4000)),
        })),
      }),
    }));

    const result = await generateBookContentCore(
      BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-7'
    );

    expect(mockGenerateWithAI).toHaveBeenCalledTimes(1);
    expect(result.chapters).toHaveLength(2);
  });

  it('rejects a thin single-pass result and rewrites the book chapter by chapter', async () => {
    const chapters = makeChapters(2);
    const sections = makeSections(chapters, 2);

    let call = 0;
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      call++;
      if (call === 1) {
        // The old bar accepted anything over 200 characters per chapter.
        return Promise.resolve({
          content: JSON.stringify({
            chapters: chapters.map((ch) => ({
              id: ch.id,
              content: '<p>A short summary of the chapter in a couple of lines only.</p>',
              sections: [],
            })),
          }),
        });
      }
      const chapterId = chapterIdFromPrompt(prompt);
      return Promise.resolve({
        content: chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 4000),
      });
    });

    const result = await generateBookContentCore(
      BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-8'
    );

    // Single pass + one call per chapter.
    expect(mockGenerateWithAI).toHaveBeenCalledTimes(3);
    result.chapters.forEach((c: any) => expect(c.content.length).toBeGreaterThan(500));
  });

  it('still produces a book when a chapter fails outright, without duplicating chapters', async () => {
    const chapters = makeChapters(3);
    const sections = makeSections(chapters, 2);
    mockGenerateWithAI.mockImplementation((prompt: string) => {
      const chapterId = chapterIdFromPrompt(prompt);
      if (chapterId === 'ch-2') return Promise.reject(new Error('provider timeout'));
      return Promise.resolve({
        content: chapterResponse(chapterId, sections[chapterId].map((s: any) => s.id), 3500),
      });
    });

    const result = await generateBookContentCore(
      BOOK, chapters, sections, CONFIG(), 'c1', () => {}, 'job-9'
    );

    expect(result.chapters).toHaveLength(3);
    expect(new Set(result.chapters.map((c: any) => c.id)).size).toBe(3);
    // The failed chapter is flagged in place rather than silently saved as final.
    const failed = result.chapters.find((c: any) => c.id === 'ch-2');
    expect(failed.status).not.toBe('final');
    expect(failed.content).toMatch(/was not received/i);
  });
});
