import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';
import type { components } from '$lib/generated/api';

const { default: JourneyReader } = await import('./JourneyReader.svelte');

afterEach(cleanup);

declare global {
	interface Window {
		__xss_journey?: number;
	}
}

type GeschichteView = components['schemas']['GeschichteView'];
type JourneyItemView = components['schemas']['JourneyItemView'];

const baseGeschichte = (overrides: Partial<GeschichteView> = {}): GeschichteView => ({
	id: 'g1',
	title: 'Lesereise Berlin',
	body: null as unknown as undefined,
	type: 'JOURNEY',
	status: 'PUBLISHED',
	persons: [],
	items: [],
	createdAt: '2026-01-01T00:00:00Z',
	updatedAt: '2026-01-01T00:00:00Z',
	...overrides
});

const docItem = (id: string, title: string, position: number, note?: string): JourneyItemView => ({
	id,
	position,
	document: {
		id: `d${id}`,
		title,
		datePrecision: 'DAY',
		documentDate: '1923-05-15',
		receiverCount: 0
	},
	note
});

const interludeItem = (id: string, note: string, position: number): JourneyItemView => ({
	id,
	position,
	document: undefined,
	note
});

const ctx = () => new Map([[CONFIRM_KEY, createConfirmService()]]);

describe('JourneyReader', () => {
	it('renders intro paragraph when body is non-empty', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: { geschichte: baseGeschichte({ body: 'Eine Reise durch die Geschichte.' }) }
		});

		await expect.element(page.getByText('Eine Reise durch die Geschichte.')).toBeVisible();
	});

	it('intro paragraph uses readable body size (text-lg, #800)', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: { geschichte: baseGeschichte({ body: 'Eine Reise durch die Geschichte.' }) }
		});

		const intro = document.querySelector('p');
		expect(intro!.className).toContain('text-lg');
		expect(intro!.className).not.toContain('text-sm');
	});

	it('omits intro paragraph when body is null', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: { geschichte: baseGeschichte({ body: undefined }) }
		});

		// Only empty state should render
		await expect.element(page.getByTestId('journey-empty-state')).toBeVisible();
	});

	it('omits intro paragraph when body is only whitespace', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: { geschichte: baseGeschichte({ body: '   ' }) }
		});

		// Whitespace-only body must NOT produce a visible intro paragraph.
		// The only rendered content should be the empty-state message.
		await expect.element(page.getByTestId('journey-empty-state')).toBeVisible();
		const paragraphs = document.querySelectorAll('p:not([data-testid])');
		expect(paragraphs.length).toBe(0);
	});

	it('renders empty-state message when items array is empty', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: { geschichte: baseGeschichte({ items: [] }) }
		});

		await expect.element(page.getByText('Diese Lesereise ist noch leer.')).toBeVisible();
	});

	it('renders both intro and empty-state when body is set but items is empty', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: {
				geschichte: baseGeschichte({ body: 'Eine Einleitung.', items: [] })
			}
		});

		await expect.element(page.getByText('Eine Einleitung.')).toBeVisible();
		await expect.element(page.getByText('Diese Lesereise ist noch leer.')).toBeVisible();
	});

	it('renders document items (JourneyItemCard)', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: {
				geschichte: baseGeschichte({ items: [docItem('item1', 'Brief an Helene', 0)] })
			}
		});

		await expect.element(page.getByText('Brief an Helene')).toBeVisible();
	});

	it('renders interlude items (JourneyInterlude)', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: {
				geschichte: baseGeschichte({ items: [interludeItem('inter1', 'Eine Pause.', 0)] })
			}
		});

		await expect.element(page.getByText('Eine Pause.')).toBeVisible();
	});

	it('omits items where document is null AND note is blank (dangling-item rule)', async () => {
		render(JourneyReader, {
			context: ctx(),
			props: {
				geschichte: baseGeschichte({
					items: [
						{ id: 'dangling', position: 0, document: undefined, note: '   ' },
						docItem('item2', 'Echter Brief', 1)
					]
				})
			}
		});

		await expect.element(page.getByText('Echter Brief')).toBeVisible();
		// Empty-state must NOT render when valid items exist
		await expect.element(page.getByText('Diese Lesereise ist noch leer.')).not.toBeInTheDocument();
	});

	it('XSS: Journey body is rendered as plaintext — injected payload does not execute', async () => {
		// JourneyReader uses Svelte text interpolation, NOT {@html}.
		render(JourneyReader, {
			context: ctx(),
			props: {
				geschichte: baseGeschichte({
					body: '<img src=x onerror="window.__xss_journey=1">'
				})
			}
		});

		expect(window.__xss_journey).toBeUndefined();
		expect(document.body.textContent).toContain('<img src=x onerror=');
	});
});
