import { expect } from '@playwright/test';
import type { APIRequestContext, Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../../playwright/config';

// ──────────────────────────────────────────────────
// Types
// ──────────────────────────────────────────────────

export interface TestContext {
    request: APIRequestContext;
    orgId: string;
    memberId: string;
}

// ──────────────────────────────────────────────────
// Auth helpers
// ──────────────────────────────────────────────────

/**
 * Create a Passport API token by calling the token endpoint from the browser.
 *
 * The browser's native fetch includes the laravel_token cookie (set by
 * CreateFreshApiToken during the dashboard page load), so authentication
 * is handled by the browser's own cookie jar. The returned Bearer token is
 * then used for all subsequent API calls, making them independent of cookie state.
 *
 * If the first attempt returns 401 (Octane hasn't fully committed the session yet),
 * we reload the page to trigger a fresh CreateFreshApiToken and retry.
 */
async function createApiToken(page: Page): Promise<string> {
    for (let attempt = 0; attempt < 3; attempt++) {
        const result = await page.evaluate(async (baseUrl) => {
            const xsrfCookie = document.cookie.split('; ').find((c) => c.startsWith('XSRF-TOKEN='));
            const xsrfToken = xsrfCookie
                ? decodeURIComponent(xsrfCookie.split('=').slice(1).join('='))
                : '';

            const res = await fetch(`${baseUrl}/api/v1/users/me/api-tokens`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    Accept: 'application/json',
                    'X-XSRF-TOKEN': xsrfToken,
                },
                body: JSON.stringify({ name: 'playwright-test' }),
            });

            if (!res.ok) {
                return null;
            }

            const body = await res.json();
            return body.data.access_token as string;
        }, PLAYWRIGHT_BASE_URL);

        if (result) {
            return result;
        }

        // Reload to get a fresh laravel_token cookie and retry.
        // networkidle gives Octane time to fully commit the session.
        await page.reload({ waitUntil: 'networkidle' });
    }

    throw new Error('Failed to create API token after retries');
}

function bearerHeaders(token: string): Record<string, string> {
    return {
        Accept: 'application/json',
        Authorization: `Bearer ${token}`,
    };
}

// ──────────────────────────────────────────────────
// Context setup
// ──────────────────────────────────────────────────

export async function setupTestContext(page: Page): Promise<TestContext> {
    const token = await createApiToken(page);
    const request = page.request;
    const headers = bearerHeaders(token);

    const orgId = await getOrganizationId(request, headers);
    const memberId = await getCurrentMemberId(request, orgId, headers);
    return { request: createAuthenticatedRequest(request, headers), orgId, memberId };
}

function createAuthenticatedRequest(
    request: APIRequestContext,
    headers: Record<string, string>
): APIRequestContext {
    // Wrap the request to always include auth headers
    return new Proxy(request, {
        get(target, prop) {
            if (
                prop === 'get' ||
                prop === 'post' ||
                prop === 'put' ||
                prop === 'delete' ||
                prop === 'patch'
            ) {
                return (url: string, options?: Record<string, unknown>) => {
                    return target[prop as 'get'](url, {
                        ...options,
                        headers: {
                            ...headers,
                            ...((options?.headers as Record<string, string>) || {}),
                        },
                    });
                };
            }
            return target[prop as keyof APIRequestContext];
        },
    });
}

async function getOrganizationId(
    request: APIRequestContext,
    headers: Record<string, string>
): Promise<string> {
    const response = await request.get(`${PLAYWRIGHT_BASE_URL}/api/v1/users/me/memberships`, {
        headers,
    });
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data[0].organization.id;
}

async function getCurrentMemberId(
    request: APIRequestContext,
    orgId: string,
    headers: Record<string, string>
): Promise<string> {
    const response = await request.get(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${orgId}/members`,
        { headers }
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data[0].id;
}

// ──────────────────────────────────────────────────
// Duration parsing
// ──────────────────────────────────────────────────

function parseDurationToSeconds(duration: string): number {
    let totalSeconds = 0;

    // Match patterns like "1h", "30min", "2h 30min", "1h 7min"
    const hourMatch = duration.match(/(\d+)\s*h/);
    const minMatch = duration.match(/(\d+)\s*min/);

    if (hourMatch) {
        totalSeconds += parseInt(hourMatch[1], 10) * 3600;
    }
    if (minMatch) {
        totalSeconds += parseInt(minMatch[1], 10) * 60;
    }

    // If no h/min pattern matched, try plain number as minutes
    if (!hourMatch && !minMatch) {
        const plainNumber = parseInt(duration, 10);
        if (!isNaN(plainNumber)) {
            totalSeconds = plainNumber * 60;
        }
    }

    return totalSeconds;
}

/**
 * Builds a start/end pair anchored to 09:00 UTC on today's UTC date.
 *
 * Intentionally pinned to UTC (rather than the runner's local time) so
 * the produced timestamps are identical regardless of where the suite
 * runs. Playwright test users default to UTC, so this matches what the
 * app will see and keeps day-of-week / "this week" assertions stable
 * for developers running the suite locally in non-UTC timezones.
 */
function createTimestamps(duration: string): { start: string; end: string } {
    const durationSeconds = parseDurationToSeconds(duration);
    const now = new Date();
    const start = createUtcTimestampFromDateParts(
        now.getUTCFullYear(),
        now.getUTCMonth(),
        now.getUTCDate(),
        9
    );
    const end = new Date(start.getTime() + durationSeconds * 1000);

    return {
        start: formatTimestamp(start),
        end: formatTimestamp(end),
    };
}

function formatTimestamp(date: Date): string {
    return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
}

function createUtcTimestampFromDateParts(
    year: number,
    month: number,
    date: number,
    hours: number,
    minutes: number = 0,
    seconds: number = 0
): Date {
    return new Date(Date.UTC(year, month, date, hours, minutes, seconds));
}

function createTimestampsOnDate(date: Date, duration: string): { start: string; end: string } {
    const durationSeconds = parseDurationToSeconds(duration);
    const start = createUtcTimestampFromDateParts(
        date.getUTCFullYear(),
        date.getUTCMonth(),
        date.getUTCDate(),
        9
    );
    const end = new Date(start.getTime() + durationSeconds * 1000);
    return {
        start: formatTimestamp(start),
        end: formatTimestamp(end),
    };
}

function randomColor(): string {
    const colors = [
        '#ef5350',
        '#ab47bc',
        '#5c6bc0',
        '#29b6f6',
        '#26a69a',
        '#9ccc65',
        '#ffa726',
        '#8d6e63',
    ];
    return colors[Math.floor(Math.random() * colors.length)];
}

// ──────────────────────────────────────────────────
// Entity creation
// ──────────────────────────────────────────────────

export async function createPublicProjectViaApi(
    ctx: TestContext,
    data: {
        name: string;
        is_billable?: boolean;
        billable_rate?: number | null;
        client_id?: string | null;
    }
) {
    return createProjectViaApi(ctx, {
        ...data,
        is_public: true,
    });
}

export async function createProjectViaApi(
    ctx: TestContext,
    data: {
        name: string;
        color?: string;
        is_billable?: boolean;
        billable_rate?: number | null;
        client_id?: string | null;
        estimated_time?: number | null;
        is_public?: boolean;
    }
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/projects`,
        {
            data: {
                name: data.name,
                color: data.color ?? randomColor(),
                is_billable: data.is_billable ?? false,
                billable_rate: data.billable_rate ?? null,
                client_id: data.client_id ?? null,
                estimated_time: data.estimated_time ?? null,
                is_public: data.is_public ?? false,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; name: string; color: string; is_billable: boolean };
}

export async function archiveProjectViaApi(
    ctx: TestContext,
    project: {
        id: string;
        name: string;
        color: string;
        is_billable: boolean;
        client_id?: string | null;
        billable_rate?: number | null;
        estimated_time?: number | null;
    }
) {
    const response = await ctx.request.put(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/projects/${project.id}`,
        {
            data: {
                name: project.name,
                color: project.color,
                is_billable: project.is_billable,
                is_archived: true,
                client_id: project.client_id ?? null,
                billable_rate: project.billable_rate ?? null,
                estimated_time: project.estimated_time ?? null,
            },
        }
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data;
}

export async function createBillableProjectViaApi(
    ctx: TestContext,
    data: { name: string; billable_rate?: number | null }
) {
    return createProjectViaApi(ctx, {
        name: data.name,
        is_billable: true,
        billable_rate: data.billable_rate ?? null,
    });
}

export async function createClientViaApi(ctx: TestContext, data: { name: string }) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/clients`,
        { data: { name: data.name } }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; name: string };
}

export async function createProjectWithClientViaApi(
    ctx: TestContext,
    projectName: string,
    clientName: string
) {
    const client = await createClientViaApi(ctx, { name: clientName });
    const project = await createProjectViaApi(ctx, {
        name: projectName,
        client_id: client.id,
    });
    return { project, client };
}

export async function createTaskViaApi(
    ctx: TestContext,
    data: { name: string; project_id: string }
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/tasks`,
        {
            data: {
                name: data.name,
                project_id: data.project_id,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; name: string; project_id: string };
}

export async function markTaskDoneViaApi(ctx: TestContext, task: { id: string; name: string }) {
    const response = await ctx.request.put(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/tasks/${task.id}`,
        {
            data: {
                name: task.name,
                is_done: true,
            },
        }
    );
    expect(response.status()).toBe(200);
    return (await response.json()).data;
}

export async function createTagViaApi(ctx: TestContext, data: { name: string }) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/tags`,
        { data: { name: data.name } }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; name: string };
}

export async function createTimeEntryViaApi(
    ctx: TestContext,
    data: {
        description?: string;
        duration: string;
        projectId?: string | null;
        taskId?: string | null;
        tags?: string[];
        billable?: boolean;
        type?: 'work' | 'break';
    }
) {
    const { start, end } = createTimestamps(data.duration);
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
        {
            data: {
                member_id: ctx.memberId,
                start,
                end,
                description: data.description ?? '',
                project_id: data.projectId ?? null,
                task_id: data.taskId ?? null,
                tags: data.tags ?? [],
                billable: data.billable ?? false,
                type: data.type ?? 'work',
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; start: string; end: string; description: string };
}

export async function createTimeEntryOnDateViaApi(
    ctx: TestContext,
    data: {
        date: Date;
        duration: string;
        description?: string;
        projectId?: string | null;
        taskId?: string | null;
        tags?: string[];
        billable?: boolean;
    }
) {
    const { start, end } = createTimestampsOnDate(data.date, data.duration);
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
        {
            data: {
                member_id: ctx.memberId,
                start,
                end,
                description: data.description ?? '',
                project_id: data.projectId ?? null,
                task_id: data.taskId ?? null,
                tags: data.tags ?? [],
                billable: data.billable ?? false,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; start: string; end: string; description: string };
}

export async function createProjectMemberViaApi(
    ctx: TestContext,
    projectId: string,
    data: { member_id: string; billable_rate?: number | null }
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/projects/${projectId}/project-members`,
        {
            data: {
                member_id: data.member_id,
                billable_rate: data.billable_rate ?? null,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; billable_rate: number | null };
}

export async function getMembersViaApi(ctx: TestContext) {
    const response = await ctx.request.get(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/members`
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data as Array<{
        id: string;
        name: string;
        email: string;
        role: string;
        billable_rate: number | null;
        is_placeholder: boolean;
    }>;
}

export async function updateMemberBillableRateViaApi(
    ctx: TestContext,
    memberId: string,
    billableRate: number | null
) {
    const response = await ctx.request.put(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/members/${memberId}`,
        { data: { billable_rate: billableRate } }
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data;
}

// ──────────────────────────────────────────────────
// Composite helpers (matching existing UI helper signatures)
// ──────────────────────────────────────────────────

export async function createTimeEntryWithProjectViaApi(
    ctx: TestContext,
    projectName: string,
    duration: string
) {
    const project = await createProjectViaApi(ctx, { name: projectName });
    const entry = await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName}`,
        duration,
        projectId: project.id,
    });
    return { project, entry };
}

export async function createTimeEntryWithProjectAndTaskViaApi(
    ctx: TestContext,
    projectId: string,
    taskName: string,
    projectName: string,
    duration: string
) {
    const task = await createTaskViaApi(ctx, { name: taskName, project_id: projectId });
    const entry = await createTimeEntryViaApi(ctx, {
        description: `Entry for ${projectName} - ${taskName}`,
        duration,
        projectId,
        taskId: task.id,
    });
    return { task, entry };
}

export async function createTimeEntryWithTagViaApi(
    ctx: TestContext,
    tagName: string,
    duration: string
) {
    const tag = await createTagViaApi(ctx, { name: tagName });
    const entry = await createTimeEntryViaApi(ctx, {
        description: `Entry with tag ${tagName}`,
        duration,
        tags: [tag.id],
    });
    return { tag, entry };
}

export async function createRunningTimeEntryViaApi(ctx: TestContext, description: string) {
    const start = new Date();
    start.setMinutes(start.getMinutes() - 10);
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
        {
            data: {
                member_id: ctx.memberId,
                start: formatTimestamp(start),
                description,
                billable: false,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; start: string; end: null; description: string };
}

export async function createBareTimeEntryViaApi(
    ctx: TestContext,
    description: string,
    duration: string
) {
    return createTimeEntryViaApi(ctx, { description, duration });
}

export async function createTimeEntryWithBillableStatusViaApi(
    ctx: TestContext,
    isBillable: boolean,
    duration: string
) {
    return createTimeEntryViaApi(ctx, {
        description: `Time entry ${isBillable ? 'billable' : 'non-billable'}`,
        duration,
        billable: isBillable,
    });
}

// ──────────────────────────────────────────────────
// Import helper (for placeholder member creation)
// ──────────────────────────────────────────────────

export async function createPlaceholderMemberViaImportApi(
    ctx: TestContext,
    placeholderName: string
) {
    const placeholderEmail = `placeholder+${Math.floor(Math.random() * 100000)}@solidtime-import.test`;
    const csvContent = [
        'User,Email,Client,Project,Task,Description,Billable,Start date,Start time,End date,End time,Tags',
        `${placeholderName},${placeholderEmail},,,,Imported entry,No,2024-01-01,09:00:00,2024-01-01,10:00:00,`,
    ].join('\n');

    const base64Data = Buffer.from(csvContent).toString('base64');

    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/import`,
        {
            data: {
                type: 'toggl_time_entries',
                data: base64Data,
            },
        }
    );
    expect(response.status()).toBe(200);
    return await response.json();
}

// ──────────────────────────────────────────────────
// Organization settings helpers
// ──────────────────────────────────────────────────

export async function updateOrganizationSettingViaApi(
    ctx: TestContext,
    settings: Record<string, unknown>
) {
    const response = await ctx.request.put(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}`,
        { data: settings }
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data;
}

export async function updateOrganizationCurrencyViaWeb(
    page: Page,
    ctx: TestContext,
    currency: string,
    name: string = 'Test Organization'
) {
    const cookies = await page.context().cookies();
    const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
    const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';

    const response = await page.request.put(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}`,
        {
            headers: { 'X-XSRF-TOKEN': xsrfToken },
            data: { name, currency },
        }
    );
    expect(response.status()).toBe(200);
}

// ──────────────────────────────────────────────────
// Bulk helpers
// ──────────────────────────────────────────────────

export async function createMultipleTimeEntriesViaApi(
    ctx: TestContext,
    count: number,
    data: { description?: string; duration?: string } = {}
) {
    const entries = [];
    for (let i = 0; i < count; i++) {
        const entry = await createTimeEntryViaApi(ctx, {
            description: data.description ?? `Bulk entry ${i + 1}`,
            duration: data.duration ?? '30min',
        });
        entries.push(entry);
    }
    return entries;
}

// ──────────────────────────────────────────────────
// Invitation helpers
// ──────────────────────────────────────────────────

export async function getInvitationsViaApi(ctx: TestContext) {
    const response = await ctx.request.get(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invitations`
    );

    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data as Array<{ id: string; email: string; role: string }>;
}

// ──────────────────────────────────────────────────
// Timestamp-based time entry helpers
// ──────────────────────────────────────────────────

/**
 * Creates a time entry on `date` at a specific UTC hour with a duration
 * in seconds. Playwright test users default to the UTC timezone, so this
 * keeps time-placement scenarios stable across runner locales.
 */
export async function createTimeEntryAtHourViaApi(
    ctx: TestContext,
    data: {
        date: Date;
        startHour: number;
        startMinute?: number;
        durationSeconds: number;
        projectId?: string | null;
        taskId?: string | null;
        description?: string;
    }
) {
    const start = createUtcTimestampFromDateParts(
        data.date.getUTCFullYear(),
        data.date.getUTCMonth(),
        data.date.getUTCDate(),
        data.startHour,
        data.startMinute ?? 0
    );
    const end = new Date(start.getTime() + data.durationSeconds * 1000);
    return createTimeEntryWithTimestampsViaApi(ctx, {
        start: formatTimestamp(start),
        end: formatTimestamp(end),
        projectId: data.projectId ?? null,
        taskId: data.taskId ?? null,
        description: data.description ?? '',
    });
}

/**
 * Reads time entries for the current member, optionally filtered to a
 * date range. Returns the raw API objects (id, start, end, project_id,
 * etc.) so tests can assert on the database state after a UI action.
 */
export async function getTimeEntriesViaApi(
    ctx: TestContext,
    filters: { start?: string; end?: string } = {}
): Promise<
    Array<{
        id: string;
        start: string;
        end: string | null;
        duration: number | null;
        project_id: string | null;
        task_id: string | null;
        description: string;
        type: 'work' | 'break';
    }>
> {
    const params = new URLSearchParams();
    params.set('member_id', ctx.memberId);
    if (filters.start) params.set('start', filters.start);
    if (filters.end) params.set('end', filters.end);

    const response = await ctx.request.get(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries?${params.toString()}`
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data;
}

export async function createTimeEntryWithTimestampsViaApi(
    ctx: TestContext,
    data: {
        description?: string;
        start: string;
        end: string;
        projectId?: string | null;
        taskId?: string | null;
        tags?: string[];
        billable?: boolean;
        type?: 'work' | 'break';
    }
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
        {
            data: {
                member_id: ctx.memberId,
                start: data.start,
                end: data.end,
                description: data.description ?? '',
                project_id: data.projectId ?? null,
                task_id: data.taskId ?? null,
                tags: data.tags ?? [],
                billable: data.billable ?? false,
                type: data.type ?? 'work',
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as {
        id: string;
        start: string;
        end: string;
        description: string;
        type: 'work' | 'break';
    };
}

// ──────────────────────────────────────────────────
// User profile helpers
// ──────────────────────────────────────────────────

export async function getCurrentUserViaApi(ctx: TestContext) {
    const response = await ctx.request.get(`${PLAYWRIGHT_BASE_URL}/api/v1/users/me`);
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data as {
        id: string;
        name: string;
        email: string;
        timezone: string;
        week_start: string;
    };
}

export async function updateUserProfileViaApi(
    ctx: TestContext,
    settings: { timezone?: string; week_start?: string }
) {
    const user = await getCurrentUserViaApi(ctx);

    // Only send the fields under test; the endpoint leaves omitted fields untouched.
    const data: Record<string, string> = {};
    if (settings.timezone !== undefined) {
        data.timezone = settings.timezone;
    }
    if (settings.week_start !== undefined) {
        data.week_start = settings.week_start;
    }

    const response = await ctx.request.put(`${PLAYWRIGHT_BASE_URL}/api/v1/users/${user.id}`, {
        data,
    });
    expect(response.status()).toBe(200);
}

// ──────────────────────────────────────────────────
// Running time entry with specific start
// ──────────────────────────────────────────────────

export async function createRunningTimeEntryWithStartViaApi(
    ctx: TestContext,
    description: string,
    start: string
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`,
        {
            data: {
                member_id: ctx.memberId,
                start,
                description,
                billable: false,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; start: string; end: null; description: string };
}

// ──────────────────────────────────────────────────
// Reports
// ──────────────────────────────────────────────────

export async function createReportViaApi(
    ctx: TestContext,
    data: {
        name: string;
        is_public?: boolean;
        public_until?: string | null;
    }
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/reports`,
        {
            data: {
                name: data.name,
                description: '',
                is_public: data.is_public ?? true,
                public_until: data.public_until ?? null,
                properties: {
                    start: '2024-01-01T00:00:00Z',
                    end: '2030-12-31T23:59:59Z',
                    group: 'project',
                    sub_group: 'project',
                    history_group: 'day',
                },
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as {
        id: string;
        name: string;
        is_public: boolean;
        public_until: string | null;
    };
}

// ──────────────────────────────────────────────────
// Invoices
// ──────────────────────────────────────────────────

export async function createInvoiceViaApi(
    ctx: TestContext,
    data: {
        reference: string;
        buyer_name?: string;
        seller_name?: string;
        currency?: string;
        date?: string;
        tax_rate?: number;
    }
) {
    const response = await ctx.request.post(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invoices`,
        {
            data: {
                seller_name: data.seller_name ?? 'Test Seller',
                buyer_name: data.buyer_name ?? 'Test Buyer',
                reference: data.reference,
                currency: data.currency ?? 'EUR',
                date: data.date ?? new Date().toISOString().split('T')[0],
                // Mirror the UI create form, which always sends a tax rate (default 0).
                // Invoices with a null tax_rate currently crash PDF rendering.
                tax_rate: data.tax_rate ?? 0,
            },
        }
    );
    expect(response.status()).toBe(201);
    const body = await response.json();
    return body.data as { id: string; reference: string; buyer_name: string };
}

export async function updateInvoiceSettingsViaApi(ctx: TestContext, data: Record<string, unknown>) {
    const response = await ctx.request.put(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invoice-settings`,
        { data }
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data as Record<string, unknown>;
}

export async function getInvoiceSettingsViaApi(ctx: TestContext) {
    const response = await ctx.request.get(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invoice-settings`
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data as Record<string, unknown>;
}

export async function getInvoicesViaApi(ctx: TestContext) {
    const response = await ctx.request.get(
        `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invoices`
    );
    expect(response.status()).toBe(200);
    const body = await response.json();
    return body.data as Array<{
        id: string;
        reference: string;
        buyer_name: string;
        paid_date: string | null;
    }>;
}
