import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { createCrmUiFieldComponents } from '../src/ui-field-components';
import type { CrmUiFieldDefinition } from '../src/ui-field-components';
import { blockRemoteContent } from '../src/platform/core/utils/htmlUtils';
import {
  CRM_EMAIL_MESSAGE_HTML_BODY_FIELD,
  CRM_EMAIL_MESSAGE_OBJECT,
} from '../src/ui-field-surface-policy';

// Stands in for the platform sanitize, which is DOMPurify (XSS) composed with
// blockRemoteContent (privacy). Only the DOMPurify half is stubbed — the real
// blocker runs, so this exercises the actual barrier rather than a mock of it
// that could drift from the code it stands for.
const sanitizeHtml = (value: string, options?: { blockRemoteContent?: boolean }): string => {
  const xssSafe = value.replace(/<script[\s\S]*?<\/script>/gi, '');
  return options?.blockRemoteContent ? blockRemoteContent(xssSafe) : xssSafe;
};

function components() {
  return createCrmUiFieldComponents({
    SearchableLookup: () => null,
    RecordLink: () => null,
    decodeHtmlEntities: value => value,
    sanitizeHtml,
    htmlToText: value => value.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(),
    formatCurrency: value => String(value),
    formatDate: value => value,
    formatDateTime: value => value,
    getRecordValue: (record, fieldName) => record?.[fieldName],
    parseMultipicklistSelections: value => (Array.isArray(value) ? value.map(String) : []),
    joinMultipicklistSelections: values => values.join(';'),
  });
}

function emailHtmlRenderer() {
  const { fieldRendererOverrides } = components();
  const override = fieldRendererOverrides.find(
    o => o.objectApiName === CRM_EMAIL_MESSAGE_OBJECT && o.fieldApiName === CRM_EMAIL_MESSAGE_HTML_BODY_FIELD,
  );
  if (!override) throw new Error('EmailMessage.HtmlBody renderer override is not registered');
  return override.renderer;
}

const htmlBody: CrmUiFieldDefinition = { name: 'HtmlBody', label: 'HTML Body', type: 'textarea' };

const TRACKED = '<div dir="ltr"><p>Thanks for the update.</p>'
  + '<img src="https://tracker.example/pixel.gif" width="1" height="1">'
  + '</div>';

describe('EmailMessage.HtmlBody renderer', () => {
  it('is registered as an (object, field) override so it wins over the shared rich-text renderer', () => {
    const { fieldRendererOverrides } = components();
    expect(fieldRendererOverrides.map(o => `${o.objectApiName}.${o.fieldApiName}`)).toContain('EmailMessage.HtmlBody');
  });

  it('renders the email as rich HTML but withholds remote content until asked', () => {
    const Email = emailHtmlRenderer();
    const { container } = render(<Email field={htmlBody} value={TRACKED} variant="detail" />);

    // The body renders as real markup, not escaped source text.
    expect(container.querySelector('p')?.textContent).toBe('Thanks for the update.');
    expect(container.innerHTML).not.toContain('&lt;p&gt;');
    // ...but the tracking pixel has no source, so opening the record fetches nothing.
    const img = container.querySelector('img');
    expect(img).not.toBeNull();
    expect(img?.getAttribute('src')).toBeNull();
    expect(container.innerHTML).not.toContain('tracker.example');
    expect(screen.getByText(/Remote content .* is blocked/i)).toBeInTheDocument();
  });

  it('loads remote content only after the reader opts in', () => {
    const Email = emailHtmlRenderer();
    const { container } = render(<Email field={htmlBody} value={TRACKED} variant="detail" />);

    fireEvent.click(screen.getByRole('button', { name: /load remote content/i }));

    expect(container.querySelector('img')?.getAttribute('src')).toBe('https://tracker.example/pixel.gif');
    // The offer is spent — no lingering banner once content is loaded.
    expect(screen.queryByRole('button', { name: /load remote content/i })).not.toBeInTheDocument();
  });

  it('asks again for the next email instead of reusing the previous opt-in', () => {
    // Regression: the detail surface reuses this component across records, so
    // consent held in mount-scoped state would silently carry over — open one
    // email, opt in, and every later sender gets fetched unasked.
    const Email = emailHtmlRenderer();
    const NEXT = '<div><p>Different sender.</p><img src="https://evil.example/beacon.gif"></div>';
    const { container, rerender } = render(<Email field={htmlBody} value={TRACKED} variant="detail" />);

    fireEvent.click(screen.getByRole('button', { name: /load remote content/i }));
    expect(container.querySelector('img')?.getAttribute('src')).toBe('https://tracker.example/pixel.gif');

    // Same component instance, next record's body.
    rerender(<Email field={htmlBody} value={NEXT} variant="detail" />);

    expect(container.querySelector('img')?.getAttribute('src')).toBeNull();
    expect(container.innerHTML).not.toContain('evil.example');
    expect(screen.getByRole('button', { name: /load remote content/i })).toBeInTheDocument();
  });

  it('asks again for a different record even when the body is byte-identical', () => {
    // Regression: keying consent on body alone lets two records that happen to
    // share HtmlBody reuse one opt-in — opening the second fetches without a
    // click. Consent is keyed on the record id too, so each record is asked.
    const Email = emailHtmlRenderer();
    const { container, rerender } = render(
      <Email field={htmlBody} record={{ Id: 'a' }} value={TRACKED} variant="detail" />,
    );

    fireEvent.click(screen.getByRole('button', { name: /load remote content/i }));
    expect(container.querySelector('img')?.getAttribute('src')).toBe('https://tracker.example/pixel.gif');

    // Same component instance and identical body, but a different record.
    rerender(<Email field={htmlBody} record={{ Id: 'b' }} value={TRACKED} variant="detail" />);

    expect(container.querySelector('img')?.getAttribute('src')).toBeNull();
    expect(screen.getByRole('button', { name: /load remote content/i })).toBeInTheDocument();
  });

  it('keeps inline (cid:/data:) references, which never touch the network', () => {
    const Email = emailHtmlRenderer();
    const inline = '<p>Sig</p><img src="cid:logo@corp"><img src="data:image/gif;base64,R0lGOD">';
    const { container } = render(<Email field={htmlBody} value={inline} variant="detail" />);

    expect(container.querySelectorAll('img[src]')).toHaveLength(2);
    // Nothing was withheld, so the reader is not prompted about remote content.
    expect(screen.queryByRole('button', { name: /load remote content/i })).not.toBeInTheDocument();
  });

  it('does not offer the opt-in for an email with no remote references', () => {
    const Email = emailHtmlRenderer();
    render(<Email field={htmlBody} value={'<p>Plain enough.</p>'} variant="detail" />);

    expect(screen.getByText('Plain enough.')).toBeInTheDocument();
    expect(screen.queryByText(/Remote content/i)).not.toBeInTheDocument();
  });

  it('stays plain text on compact surfaces, never injecting markup into a row', () => {
    const Email = emailHtmlRenderer();
    const { container } = render(<Email field={htmlBody} value={TRACKED} variant="table" />);

    expect(container.querySelector('img')).toBeNull();
    expect(container.querySelector('p')).toBeNull();
    expect(container.firstElementChild).toHaveClass('truncate');
    expect(container.textContent).toContain('Thanks for the update.');
  });

  it('renders the empty placeholder for blank or markup-only bodies', () => {
    const Email = emailHtmlRenderer();
    for (const value of ['', null, undefined, '<p><br></p>']) {
      const { unmount } = render(<Email field={htmlBody} value={value} variant="detail" />);
      expect(screen.getByText('—')).toBeInTheDocument();
      unmount();
    }
  });
});
