import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import PersonHoverCard from './PersonHoverCard.svelte';
import type { components } from '$lib/generated/api';

type Person = components['schemas']['Person'];
type RelationshipDTO = components['schemas']['RelationshipDTO'];

const AUGUSTE: Person = {
	id: 'p-aug',
	firstName: 'Auguste',
	lastName: 'Raddatz',
	displayName: 'Auguste Raddatz',
	personType: 'PERSON',
	familyMember: true,
	birthDate: '1882-01-01',
	birthDatePrecision: 'YEAR',
	deathDate: '1944-01-01',
	deathDatePrecision: 'YEAR'
} as unknown as Person;

const POSITION = { top: 100, left: 200 };

afterEach(() => cleanup());

describe('PersonHoverCard — loading state', () => {
	it('shows the skeleton when state.status is loading', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loading' }
		});
		await expect.element(page.getByTestId('person-hover-card-skeleton')).toBeInTheDocument();
	});

	it('renders three skeleton bars', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loading' }
		});
		const bars = document.querySelectorAll('[data-testid="person-hover-card-skeleton"] .bar');
		expect(bars.length).toBe(3);
	});
});

describe('PersonHoverCard — error state', () => {
	it('shows a generic error message when state.status is error', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'error' }
		});
		await expect.element(page.getByTestId('person-hover-card-error')).toBeInTheDocument();
	});

	it('still allows the link footer to navigate (link present in error state)', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'error' }
		});
		// The card root must show the footer link even when the body errored —
		// click navigation works regardless of fetch outcome.
		const link = document.querySelector('a[href="/persons/p-aug"]');
		expect(link).not.toBeNull();
	});
});

describe('PersonHoverCard — loaded state', () => {
	it('renders the person displayName as the header name', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		await expect.element(page.getByText('Auguste Raddatz')).toBeInTheDocument();
	});

	it('renders the life-date range when birth and death dates are present', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		await expect
			.element(page.getByTestId('person-hover-card-dates'))
			.toHaveTextContent('* 1882 – † 1944');
	});

	it('renders a DAY-precision birth date as a full localized date', async () => {
		const exact = {
			...AUGUSTE,
			birthDate: '1882-03-14',
			birthDatePrecision: 'DAY'
		} as unknown as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: exact, relationships: [] }
		});
		await expect
			.element(page.getByTestId('person-hover-card-dates'))
			.toHaveTextContent('14. März 1882');
	});

	it('renders APPROX-precision legacy dates with the ca. prefix', async () => {
		const approx = {
			...AUGUSTE,
			birthDate: '1882-01-01',
			birthDatePrecision: 'APPROX',
			deathDate: undefined,
			deathDatePrecision: 'UNKNOWN'
		} as unknown as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: approx, relationships: [] }
		});
		await expect.element(page.getByTestId('person-hover-card-dates')).toHaveTextContent('ca. 1882');
	});

	it('keeps the * and † glyphs out of the accessible text via aria-hidden', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		const hidden = [
			...document.querySelectorAll(
				'[data-testid="person-hover-card-dates"] span[aria-hidden="true"]'
			)
		].map((el) => el.textContent?.trim());
		expect(hidden).toContain('*');
		expect(hidden).toContain('†');
	});

	it('omits the life-date line when both dates are missing', async () => {
		const noDates = {
			...AUGUSTE,
			birthDate: undefined,
			birthDatePrecision: 'UNKNOWN',
			deathDate: undefined,
			deathDatePrecision: 'UNKNOWN'
		} as unknown as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: noDates, relationships: [] }
		});
		const dates = document.querySelector('[data-testid="person-hover-card-dates"]');
		expect(dates).toBeNull();
	});

	it('renders "geb. <alias>" when alias is set', async () => {
		const withAlias = { ...AUGUSTE, alias: 'Müller' } as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: withAlias, relationships: [] }
		});
		await expect.element(page.getByText('geb. Müller')).toBeInTheDocument();
	});

	it('omits the maiden name line when alias is null', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		const maiden = document.querySelector('[data-testid="person-hover-card-maiden"]');
		expect(maiden).toBeNull();
	});

	it('renders family relationship chips for PARENT_OF, SPOUSE_OF, SIBLING_OF only', async () => {
		const relationships: RelationshipDTO[] = [
			{
				id: 'r1',
				personId: 'p-aug',
				relatedPersonId: 'p-spouse',
				personDisplayName: 'Auguste',
				relatedPersonDisplayName: 'Otto Raddatz',
				relationType: 'SPOUSE_OF',
				fromDatePrecision: 'UNKNOWN',
				toDatePrecision: 'UNKNOWN'
			},
			{
				id: 'r2',
				personId: 'p-aug',
				relatedPersonId: 'p-friend',
				personDisplayName: 'Auguste',
				relatedPersonDisplayName: 'Karl Friend',
				relationType: 'FRIEND',
				fromDatePrecision: 'UNKNOWN',
				toDatePrecision: 'UNKNOWN'
			},
			{
				id: 'r3',
				personId: 'p-aug',
				relatedPersonId: 'p-sibling',
				personDisplayName: 'Auguste',
				relatedPersonDisplayName: 'Marie Sister',
				relationType: 'SIBLING_OF',
				fromDatePrecision: 'UNKNOWN',
				toDatePrecision: 'UNKNOWN'
			}
		];
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships }
		});
		await expect.element(page.getByText('Otto Raddatz')).toBeInTheDocument();
		await expect.element(page.getByText('Marie Sister')).toBeInTheDocument();
		// Non-family relationship type must be filtered out
		const friendChip = page.getByText('Karl Friend');
		await expect.element(friendChip).not.toBeInTheDocument();
	});

	it('shows the other person name when hovered person is the object (relatedPersonId) in a PARENT_OF row', async () => {
		// Storage: Heinrich PARENT_OF Auguste. When viewing Auguste's card,
		// the chip must show "Heinrich" (the parent), not "Auguste" (herself).
		const relationships: RelationshipDTO[] = [
			{
				id: 'r-parent',
				personId: 'p-heinrich',
				relatedPersonId: 'p-aug',
				personDisplayName: 'Heinrich Raddatz',
				relatedPersonDisplayName: 'Auguste Raddatz',
				relationType: 'PARENT_OF',
				fromDatePrecision: 'UNKNOWN',
				toDatePrecision: 'UNKNOWN'
			}
		];
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships }
		});
		await expect.element(page.getByText('Heinrich Raddatz')).toBeInTheDocument();
		// Auguste must NOT appear as her own parent chip name
		const chips = document.querySelector('[data-testid="person-hover-card-chips"]');
		expect(chips?.textContent).not.toContain('Auguste Raddatz');
	});

	it('omits the chips section entirely when no family relationships', async () => {
		const onlyFriend: RelationshipDTO[] = [
			{
				id: 'r1',
				personId: 'p-aug',
				relatedPersonId: 'p-friend',
				personDisplayName: 'Auguste',
				relatedPersonDisplayName: 'Karl Friend',
				relationType: 'FRIEND',
				fromDatePrecision: 'UNKNOWN',
				toDatePrecision: 'UNKNOWN'
			}
		];
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: onlyFriend }
		});
		const chips = document.querySelector('[data-testid="person-hover-card-chips"]');
		expect(chips).toBeNull();
	});

	it('renders notes excerpt unchanged when notes ≤ 120 characters', async () => {
		const withNotes = { ...AUGUSTE, notes: 'Born in Berlin.' } as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: withNotes, relationships: [] }
		});
		await expect.element(page.getByText('Born in Berlin.')).toBeInTheDocument();
	});

	it('truncates notes longer than 120 characters with an ellipsis (single long word)', async () => {
		// Single 150-char word with no spaces: word-boundary cut would yield nothing,
		// so fall back to a hard cut at 120 + ellipsis (Sara #7: pin the exact length).
		const long = 'x'.repeat(150);
		const withLongNotes = { ...AUGUSTE, notes: long } as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: withLongNotes, relationships: [] }
		});
		const notes = document.querySelector('[data-testid="person-hover-card-notes"]')!;
		expect(notes.textContent).toBe('x'.repeat(120) + '…');
	});

	it('truncates at the last word boundary inside the 120-char window (Leonie FINDING-04)', async () => {
		// 150-char string with spaces — must cut at the last space, not mid-word.
		const sentence = 'Sie war eine bekannte Schriftstellerin und engagierte sich '.repeat(3);
		// length is 180, last space at idx ≤120
		const withLongNotes = { ...AUGUSTE, notes: sentence } as Person;
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: withLongNotes, relationships: [] }
		});
		const notes = document.querySelector('[data-testid="person-hover-card-notes"]')!;
		const text = notes.textContent ?? '';
		// Ends with ellipsis
		expect(text.endsWith('…')).toBe(true);
		// Last char before the ellipsis is NOT a half-word — verify by checking that
		// the position right before … is the end of a word (i.e., there's no letter
		// further along in the original text immediately after our cut point).
		const cut = text.slice(0, -1); // strip the …
		// Find this cut substring in the original sentence
		const idx = sentence.indexOf(cut);
		expect(idx).toBe(0);
		const charAfterCut = sentence[cut.length];
		// The next char should be a space — confirming we cut on a boundary
		expect(charAfterCut).toBe(' ');
	});

	it('omits notes section when notes is null', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		const notes = document.querySelector('[data-testid="person-hover-card-notes"]');
		expect(notes).toBeNull();
	});

	it('footer renders an anchor link to /persons/{personId}', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		const link = document.querySelector('a[href="/persons/p-aug"]')!;
		expect(link).not.toBeNull();
	});
});

describe('PersonHoverCard — accessibility', () => {
	it('uses aria-live="polite" so screen readers announce loaded content', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loading' }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]')!;
		expect(root.getAttribute('aria-live')).toBe('polite');
	});

	it('sets aria-busy="true" while loading so SR announces the state change on load', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loading' }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]')!;
		expect(root.getAttribute('aria-busy')).toBe('true');
	});

	it('does not set aria-busy when loaded (so the loaded content is announced)', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]')!;
		// aria-busy is either absent or "false"
		const busy = root.getAttribute('aria-busy');
		expect(busy === null || busy === 'false').toBe(true);
	});

	it('names the region with the person displayName when loaded (WCAG 1.3.1)', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loaded', person: AUGUSTE, relationships: [] }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]')!;
		expect(root.getAttribute('aria-label')).toBe('Auguste Raddatz');
	});

	it('names the region with a generic loading label while loading', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: POSITION,
			state: { status: 'loading' }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]')!;
		// Region must have an accessible name in every state — axe-core flags
		// role="region" without aria-label / aria-labelledby.
		expect(root.getAttribute('aria-label')).toBeTruthy();
	});

	it('exposes the cardId as the host element id (so anchor aria-describedby works)', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-xyz',
			position: POSITION,
			state: { status: 'loading' }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]')!;
		expect(root.id).toBe('card-xyz');
	});

	it('positions itself fixed at the given top/left', async () => {
		render(PersonHoverCard, {
			personId: 'p-aug',
			cardId: 'card-1',
			position: { top: 333, left: 444 },
			state: { status: 'loading' }
		});
		const root = document.querySelector('[data-testid="person-hover-card"]') as HTMLElement;
		expect(root.style.top).toBe('333px');
		expect(root.style.left).toBe('444px');
		expect(root.style.position).toBe('fixed');
	});
});
