// #region ReportsApiTest [C:3] [TYPE Module] [SEMANTICS report, api, test, search, filter]
// @BRIEF Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.
// @RELATION DEPENDS_ON -> [ReportsApi]

// #region ReportsApiTest:Module [TYPE Function]
// @SEMANTICS: tests, reports, api-client, query-string, error-normalization
// @PURPOSE: Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.
// @LAYER Tests
// @RELATION DEPENDS_ON -> [ReportsApi]
// @INVARIANT: Pure functions produce deterministic output. Async wrappers propagate structured errors.

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

// Mock SvelteKit environment modules before any source imports
vi.mock('$env/static/public', () => ({
    PUBLIC_WS_URL: 'ws://localhost:8000'
}));

// Mock toasts to prevent import side-effects
vi.mock('$lib/toasts.svelte.js', () => ({
    addToast: vi.fn()
}));

// Mock the api module
vi.mock('../../api.js', () => ({
    api: {
        fetchApi: vi.fn()
    }
}));

import { buildReportQueryString, normalizeApiError } from '../reports.js';

// #region TestBuildReportQueryString:Class [TYPE Function]
// @PURPOSE: Validate query string construction from filter options.
// @PRE: Options object with various filter fields.
// @POST: Correct URLSearchParams string produced.
describe('buildReportQueryString', () => {
    it('returns empty string for empty options', () => {
        expect(buildReportQueryString()).toBe('');
        expect(buildReportQueryString({})).toBe('');
    });

    it('serializes page and page_size', () => {
        const qs = buildReportQueryString({ page: 2, page_size: 10 });
        expect(qs).toContain('page=2');
        expect(qs).toContain('page_size=10');
    });

    it('serializes task_types array', () => {
        const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });
        expect(qs).toContain('task_types=backup%2Cmigration');
    });

    it('serializes statuses array', () => {
        const qs = buildReportQueryString({ statuses: ['success', 'failed'] });
        expect(qs).toContain('statuses=success%2Cfailed');
    });

    it('ignores empty arrays', () => {
        const qs = buildReportQueryString({ task_types: [], statuses: [] });
        expect(qs).toBe('');
    });

    it('serializes time range and search', () => {
        const qs = buildReportQueryString({
            time_from: '2024-01-01',
            time_to: '2024-12-31',
            search: 'backup'
        });
        expect(qs).toContain('time_from=2024-01-01');
        expect(qs).toContain('time_to=2024-12-31');
        expect(qs).toContain('search=backup');
    });

    it('serializes sort options', () => {
        const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });
        expect(qs).toContain('sort_by=status');
        expect(qs).toContain('sort_order=asc');
    });

    it('handles all options combined', () => {
        const qs = buildReportQueryString({
            page: 1,
            page_size: 20,
            task_types: ['backup'],
            statuses: ['success'],
            search: 'test',
            sort_by: 'updated_at',
            sort_order: 'desc'
        });
        expect(qs).toContain('page=1');
        expect(qs).toContain('page_size=20');
        expect(qs).toContain('task_types=backup');
        expect(qs).toContain('statuses=success');
        expect(qs).toContain('search=test');
    });
});
// #endregion TestBuildReportQueryString:Class

// #region TestNormalizeApiError:Class [TYPE Function]
// @PURPOSE: Validate error normalization for UI-state mapping.
// @PRE: Various error types (Error, string, object).
// @POST: Always returns {message, code, retryable} object.
describe('normalizeApiError', () => {
    it('extracts message from Error object', () => {
        const result = normalizeApiError(new Error('Connection failed'));
        expect(result.message).toBe('Connection failed');
        expect(result.code).toBe('REPORTS_API_ERROR');
        expect(result.retryable).toBe(true);
    });

    it('uses string error directly', () => {
        const result = normalizeApiError('Something went wrong');
        expect(result.message).toBe('Something went wrong');
    });

    it('falls back to default message for null/undefined', () => {
        expect(normalizeApiError(null).message).toBe('Failed to load reports');
        expect(normalizeApiError(undefined).message).toBe('Failed to load reports');
    });

    it('falls back for object without message', () => {
        const result = normalizeApiError({ status: 500 });
        expect(result.message).toBe('Failed to load reports');
    });

    it('always includes code and retryable fields', () => {
        const result = normalizeApiError('test');
        expect(result).toHaveProperty('code');
        expect(result).toHaveProperty('retryable');
    });
});
// #endregion TestNormalizeApiError:Class

// #region TestGetReportsAsync:Class [TYPE Function]
// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.
// @PRE: api.fetchApi is mocked via vi.mock.
// @POST: Functions call correct endpoints and propagate results/errors.

describe('getReports', () => {
    let api;

    beforeEach(async () => {
        vi.clearAllMocks();
        const apiModule = await import('../../api.js');
        api = apiModule.api;
    });

    it('calls fetchApi with correct endpoint', async () => {
        const { getReports } = await import('../reports.js');
        const mockResponse = { items: [], total: 0 };
        api.fetchApi.mockResolvedValue(mockResponse);

        const result = await getReports();
        expect(api.fetchApi).toHaveBeenCalledWith('/reports');
        expect(result).toEqual(mockResponse);
    });

    it('appends query string when options provided', async () => {
        const { getReports } = await import('../reports.js');
        api.fetchApi.mockResolvedValue({ items: [] });

        await getReports({ page: 2, page_size: 5 });
        const call = api.fetchApi.mock.calls[0][0];
        expect(call).toContain('/reports?');
        expect(call).toContain('page=2');
        expect(call).toContain('page_size=5');
    });

    it('throws normalized error on failure', async () => {
        const { getReports } = await import('../reports.js');
        api.fetchApi.mockRejectedValue(new Error('Network error'));

        await expect(getReports()).rejects.toEqual(
            expect.objectContaining({
                message: 'Network error',
                code: 'REPORTS_API_ERROR'
            })
        );
    });
});

describe('getReportDetail', () => {
    let api;

    beforeEach(async () => {
        vi.clearAllMocks();
        const apiModule = await import('../../api.js');
        api = apiModule.api;
    });

    it('calls fetchApi with correct endpoint', async () => {
        const { getReportDetail } = await import('../reports.js');
        const mockDetail = { report: { report_id: 'r1' } };
        api.fetchApi.mockResolvedValue(mockDetail);

        const result = await getReportDetail('r1');
        expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');
        expect(result).toEqual(mockDetail);
    });

    it('throws normalized error on failure', async () => {
        const { getReportDetail } = await import('../reports.js');
        api.fetchApi.mockRejectedValue(new Error('Not found'));

        await expect(getReportDetail('nonexistent')).rejects.toEqual(
            expect.objectContaining({
                message: 'Not found',
                code: 'REPORTS_API_ERROR'
            })
        );
    });
});
// #endregion TestGetReportsAsync:Class

// #endregion ReportsApiTest:Module
// #endregion ReportsApiTest
