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

// Mock the Anthropic SDK before any imports that use it.
// The `create` mock is defined via vi.hoisted so it can be referenced
// in the factory function (hoisted mocks run before imports).
const mockCreate = vi.hoisted(() => vi.fn());

vi.mock('@anthropic-ai/sdk', () => ({
  default: vi.fn().mockImplementation(() => ({
    messages: {
      create: mockCreate,
    },
  })),
}));

// Import after mock is in place
import { generateProposalLines, filterCatalogue } from '../lib/anthropic.js';

const validLigne = {
  ordre: 1,
  localisation: 'Cuisine RDC',
  designation: 'Remplacement joint robinet',
  type_badge: 'forfait_reparation',
  detail: 'Joint usé',
  quantite: 1,
  unite: 'forf.',
  prix_ht: 45.0,
  product_ref: 'PLOMB-001',
  offert: false,
  flag_incomplet: false,
};

const validResponse = {
  prestation_forfaitaire: false,
  forfait_description: null,
  forfait_prix_ht: null,
  lignes: [validLigne],
};

const sampleCatalogue = [
  { ref: 'PLOMB-001', label: 'Joint robinet', price: 45.0, tva_tx: 10 },
];

function makeTextBlockResponse(text: string) {
  return {
    content: [{ type: 'text', text }],
  };
}

describe('generateProposalLines', () => {
  beforeEach(() => {
    mockCreate.mockReset();
  });

  it('returns parsed GenerateResponse when Anthropic returns valid JSON', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(validResponse))
    );

    const result = await generateProposalLines(
      'Cuisine: robinet qui fuit, joint à refaire.',
      [],
      sampleCatalogue,
      'residentiel'
    );

    expect(result.prestation_forfaitaire).toBe(false);
    expect(result.lignes).toHaveLength(1);
    expect(result.lignes[0].designation).toBe('Remplacement joint robinet');
    expect(result.lignes[0].product_ref).toBe('PLOMB-001');
  });

  it('strips markdown backticks before parsing JSON', async () => {
    const jsonWithMarkdown =
      '```json\n' + JSON.stringify(validResponse) + '\n```';

    mockCreate.mockResolvedValue(makeTextBlockResponse(jsonWithMarkdown));

    const result = await generateProposalLines(
      'Cuisine: robinet qui fuit, joint à refaire.',
      [],
      sampleCatalogue,
      'residentiel'
    );

    expect(result.lignes).toHaveLength(1);
  });

  it('strips plain backtick fences before parsing JSON', async () => {
    const jsonWithFence =
      '```\n' + JSON.stringify(validResponse) + '\n```';

    mockCreate.mockResolvedValue(makeTextBlockResponse(jsonWithFence));

    const result = await generateProposalLines(
      'Cuisine: robinet qui fuit, joint à refaire.',
      [],
      sampleCatalogue,
      'residentiel'
    );

    expect(result.lignes).toHaveLength(1);
  });

  it('throws with code PARSE_ERROR when JSON is malformed', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse('this is not valid json {{{')
    );

    await expect(
      generateProposalLines(
        'Cuisine: robinet qui fuit, joint à refaire.',
        [],
        sampleCatalogue,
        'residentiel'
      )
    ).rejects.toMatchObject({ code: 'PARSE_ERROR' });
  });

  it('throws with code PARSE_ERROR when lignes property is missing', async () => {
    const noLignes = {
      prestation_forfaitaire: false,
      forfait_description: null,
      forfait_prix_ht: null,
      // lignes intentionally omitted
    };

    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(noLignes))
    );

    await expect(
      generateProposalLines(
        'Cuisine: robinet qui fuit, joint à refaire.',
        [],
        sampleCatalogue,
        'residentiel'
      )
    ).rejects.toMatchObject({ code: 'PARSE_ERROR' });
  });

  it('throws with code PARSE_ERROR when lignes is not an array', async () => {
    const badLignes = {
      prestation_forfaitaire: false,
      forfait_description: null,
      forfait_prix_ht: null,
      lignes: 'not an array',
    };

    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(badLignes))
    );

    await expect(
      generateProposalLines(
        'Cuisine: robinet qui fuit, joint à refaire.',
        [],
        sampleCatalogue,
        'residentiel'
      )
    ).rejects.toMatchObject({ code: 'PARSE_ERROR' });
  });

  it('handles photos by passing them through to the Anthropic call', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(validResponse))
    );

    await generateProposalLines(
      'Notes with photos.',
      ['data:image/jpeg;base64,/9j/abc'],
      sampleCatalogue,
      'bureau'
    );

    expect(mockCreate).toHaveBeenCalledOnce();
    const callArgs = mockCreate.mock.calls[0][0];
    // The content array should contain image block + text block
    const content = callArgs.messages[0].content;
    expect(content.some((b: { type: string }) => b.type === 'image')).toBe(true);
    expect(content.some((b: { type: string }) => b.type === 'text')).toBe(true);
  });

  it('uses concise verbosity by default', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(validResponse))
    );

    await generateProposalLines('robinet qui fuit', [], [], 'residentiel');

    const systemPrompt = mockCreate.mock.calls[0][0].system as string;
    expect(systemPrompt).toContain('1 phrase');
    expect(systemPrompt).not.toContain('3-5 phrases');
  });

  it('uses detailed verbosity when specified', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(validResponse))
    );

    await generateProposalLines('robinet qui fuit', [], [], 'residentiel', 'detailed');

    const systemPrompt = mockCreate.mock.calls[0][0].system as string;
    expect(systemPrompt).toContain('3-5 phrases');
  });

  it('includes catalogue in system prompt when provided', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(validResponse))
    );

    await generateProposalLines('robinet qui fuit', [], sampleCatalogue, 'residentiel');

    const systemPrompt = mockCreate.mock.calls[0][0].system as string;
    expect(systemPrompt).toContain('CATALOGUE DE RÉFÉRENCE');
    expect(systemPrompt).toContain('PLOMB-001');
  });

  it('omits catalogue when empty array is passed', async () => {
    mockCreate.mockResolvedValue(
      makeTextBlockResponse(JSON.stringify(validResponse))
    );

    await generateProposalLines('robinet qui fuit', [], [], 'residentiel');

    const systemPrompt = mockCreate.mock.calls[0][0].system as string;
    expect(systemPrompt).toContain('lignes libres');
    expect(systemPrompt).not.toContain('CATALOGUE DE RÉFÉRENCE');
  });
});

describe('filterCatalogue', () => {
  const makeCatalogue = (n: number) =>
    Array.from({ length: n }, (_, i) => ({
      ref:    `REF-${i}`,
      label:  `Produit générique ${i}`,
      price:  10,
      tva_tx: 20,
    }));

  it('returns catalogue unchanged when ≤50 items', () => {
    const cat = makeCatalogue(30);
    expect(filterCatalogue(cat, 'quelques notes')).toHaveLength(30);
  });

  it('returns exactly 50 items when catalogue > 50 items', () => {
    const cat = makeCatalogue(200);
    expect(filterCatalogue(cat, 'quelques notes')).toHaveLength(50);
  });

  it('ranks matching items above non-matching items', () => {
    const cat = [
      { ref: 'Z', label: 'Joint robinet plomberie', price: 10, tva_tx: 10 },
      ...makeCatalogue(60),
    ];
    const result = filterCatalogue(cat, 'robinet qui fuit joint à refaire');
    // "Joint robinet plomberie" shares tokens with notes → should be in top 50
    expect(result.some(i => i.ref === 'Z')).toBe(true);
  });

  it('returns 50 items even when no item matches notes keywords', () => {
    const cat = makeCatalogue(100);
    const result = filterCatalogue(cat, 'xyz abc def');
    expect(result).toHaveLength(50);
  });
});
