import { describe, it, expect, beforeEach, vi } from 'vitest';
import Fastify from 'fastify';
import { statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import * as fsPromises from 'node:fs/promises';
import { projectConfigRoutes } from '../../../../../../../modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.js';
import { UpdateProjectConfigUseCase } from '../../../../../../../modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.js';
import { StubProjectConfigGateway } from '../../../../../../../tests/stubs/projectConfigGateway.stub.js';
import { ProjectConfigFactory } from '../../../../../../../tests/factories/projectConfig.factory.js';
vi.mock('node:fs/promises', async () => {
    const actual = await vi.importActual('node:fs/promises');
    return {
        ...actual,
        readFile: vi.fn(),
        stat: vi.fn(),
    };
});
async function buildApp() {
    const app = Fastify();
    await app.register(projectConfigRoutes);
    return app;
}
function jsonResponse(body) {
    return JSON.stringify(body);
}
function fakeStats() {
    const stats = statSync(tmpdir());
    if (!stats) {
        throw new Error('statSync(tmpdir()) returned undefined — test environment is broken');
    }
    return stats;
}
describe('projectConfigRoutes — GET /api/project-config', () => {
    beforeEach(() => {
        vi.resetAllMocks();
    });
    it('returns the config payload including reviewFocus when present', async () => {
        vi.mocked(fsPromises.readFile).mockResolvedValue(jsonResponse(ProjectConfigFactory.create({
            reviewFocus: 'back',
            reviewSkill: undefined,
        })));
        vi.mocked(fsPromises.stat).mockResolvedValue(fakeStats());
        const app = await buildApp();
        const response = await app.inject({
            method: 'GET',
            url: '/api/project-config?path=/fake/project',
        });
        const payload = response.json();
        expect(payload.success).toBe(true);
        expect(payload.config.reviewFocus).toBe('back');
        await app.close();
    });
    it('accepts a config without reviewSkill when reviewFocus is set', async () => {
        vi.mocked(fsPromises.readFile).mockResolvedValue(jsonResponse(ProjectConfigFactory.create({
            reviewFocus: 'doc',
            reviewSkill: undefined,
        })));
        vi.mocked(fsPromises.stat).mockResolvedValue(fakeStats());
        const app = await buildApp();
        const response = await app.inject({
            method: 'GET',
            url: '/api/project-config?path=/fake/project',
        });
        const payload = response.json();
        expect(payload.success).toBe(true);
        await app.close();
    });
    it('rejects an invalid reviewFocus value', async () => {
        vi.mocked(fsPromises.readFile).mockResolvedValue(jsonResponse(ProjectConfigFactory.create({
            reviewFocus: 'mobile',
        })));
        const app = await buildApp();
        const response = await app.inject({
            method: 'GET',
            url: '/api/project-config?path=/fake/project',
        });
        const payload = response.json();
        expect(payload.success).toBe(false);
        expect(payload.error).toMatch(/Invalid reviewFocus/);
        await app.close();
    });
    it('still requires reviewSkill when reviewFocus is absent', async () => {
        vi.mocked(fsPromises.readFile).mockResolvedValue(jsonResponse({
            github: true,
            gitlab: false,
            defaultModel: 'sonnet',
            reviewFollowupSkill: 'review-followup',
        }));
        const app = await buildApp();
        const response = await app.inject({
            method: 'GET',
            url: '/api/project-config?path=/fake/project',
        });
        const payload = response.json();
        expect(payload.success).toBe(false);
        expect(payload.error).toMatch(/reviewSkill/);
        await app.close();
    });
    it('checks the SKILL.md derived from reviewFocus when reviewSkill is absent', async () => {
        vi.mocked(fsPromises.readFile).mockResolvedValue(jsonResponse(ProjectConfigFactory.create({
            reviewFocus: 'back',
            reviewSkill: undefined,
        })));
        const statMock = vi.mocked(fsPromises.stat);
        statMock.mockResolvedValue(fakeStats());
        const app = await buildApp();
        await app.inject({
            method: 'GET',
            url: '/api/project-config?path=/fake/project',
        });
        const checkedPaths = statMock.mock.calls.map((call) => String(call[0]));
        expect(checkedPaths.some((checkedPath) => checkedPath.includes('review-back/SKILL.md'))).toBe(true);
        await app.close();
    });
    it('returns a clear error when the derived SKILL.md does not exist', async () => {
        vi.mocked(fsPromises.readFile).mockResolvedValue(jsonResponse(ProjectConfigFactory.create({
            reviewFocus: 'back',
            reviewSkill: undefined,
        })));
        vi.mocked(fsPromises.stat).mockRejectedValue(new Error('ENOENT'));
        const app = await buildApp();
        const response = await app.inject({
            method: 'GET',
            url: '/api/project-config?path=/fake/project',
        });
        const payload = response.json();
        expect(payload.success).toBe(false);
        expect(payload.error).toMatch(/review-back/);
        await app.close();
    });
});
function baseConfig(overrides = {}) {
    return {
        github: false,
        gitlab: true,
        defaultModel: 'sonnet',
        reviewSkill: 'review-front',
        reviewFollowupSkill: 'review-followup',
        language: 'fr',
        retentionDays: 14,
        ...overrides,
    };
}
async function buildAppWithPatch(gateway) {
    const app = Fastify();
    const updateProjectConfig = new UpdateProjectConfigUseCase(gateway);
    await app.register(projectConfigRoutes, { updateProjectConfig });
    return app;
}
describe('projectConfigRoutes — PATCH /api/project-config', () => {
    it('returns 400 when the path query parameter is missing', async () => {
        const gateway = new StubProjectConfigGateway();
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config',
            payload: { language: 'en' },
        });
        expect(response.statusCode).toBe(400);
        await app.close();
    });
    it('returns 400 when the path contains "..":', async () => {
        const gateway = new StubProjectConfigGateway();
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/../etc'),
            payload: { language: 'en' },
        });
        expect(response.statusCode).toBe(400);
        await app.close();
    });
    it('returns 200 + { success: true, config } on a successful update', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { language: 'en' },
        });
        expect(response.statusCode).toBe(200);
        const body = response.json();
        expect(body.success).toBe(true);
        expect(body.config.language).toBe('en');
        await app.close();
    });
    it('returns 400 + French message when externalLink is http://', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { externalLink: 'http://insecure' },
        });
        expect(response.statusCode).toBe(400);
        expect(response.json().error).toBe('Le lien doit être en HTTPS');
        await app.close();
    });
    it('returns 404 when the project has no config', async () => {
        const gateway = new StubProjectConfigGateway();
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/unknown'),
            payload: { language: 'en' },
        });
        expect(response.statusCode).toBe(404);
        await app.close();
    });
    it('returns 422 + "Configuration projet illisible" when the file is malformed', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        gateway.forceMalformed('/repo/A');
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { language: 'en' },
        });
        expect(response.statusCode).toBe(422);
        expect(response.json().error).toBe('Configuration projet illisible');
        await app.close();
    });
    it('returns 500 + "Échec de la sauvegarde" when the gateway write fails', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        gateway.forceIoError();
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { language: 'en' },
        });
        expect(response.statusCode).toBe(500);
        expect(response.json().error).toBe('Échec de la sauvegarde');
        await app.close();
    });
    it('accepts a numeric qualityThreshold in the patch body', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { qualityThreshold: 7 },
        });
        expect(response.statusCode).toBe(200);
        expect(response.json().config.qualityThreshold).toBe(7);
        await app.close();
    });
    it('returns 400 when qualityThreshold is out of range', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { qualityThreshold: 15 },
        });
        expect(response.statusCode).toBe(400);
        expect(response.json().error).toMatch(/qualityThreshold/);
        await app.close();
    });
    it('clears qualityThreshold from the persisted config when patch sends null', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig({ qualityThreshold: 7 }));
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { qualityThreshold: null },
        });
        expect(response.statusCode).toBe(200);
        expect(response.json().config.qualityThreshold).toBeUndefined();
        await app.close();
    });
    it('accepts a numeric maxConcurrentReviews in the patch body', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: 4 },
        });
        expect(response.statusCode).toBe(200);
        expect(response.json().config.maxConcurrentReviews).toBe(4);
        await app.close();
    });
    it('accepts a string integer "4" for maxConcurrentReviews', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: '4' },
        });
        expect(response.statusCode).toBe(200);
        expect(response.json().config.maxConcurrentReviews).toBe(4);
        await app.close();
    });
    it('returns 400 with French range message when maxConcurrentReviews is 0', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: 0 },
        });
        expect(response.statusCode).toBe(400);
        expect(response.json().error).toBe('La valeur doit être comprise entre 1 et 10');
        await app.close();
    });
    it('returns 400 with French range message when maxConcurrentReviews is 11', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: 11 },
        });
        expect(response.statusCode).toBe(400);
        expect(response.json().error).toBe('La valeur doit être comprise entre 1 et 10');
        await app.close();
    });
    it('returns 400 with French integer message when maxConcurrentReviews is "abc"', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: 'abc' },
        });
        expect(response.statusCode).toBe(400);
        expect(response.json().error).toBe('La valeur doit être un nombre entier');
        await app.close();
    });
    it('returns 400 with French required message when maxConcurrentReviews is empty string', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig());
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: '' },
        });
        expect(response.statusCode).toBe(400);
        expect(response.json().error).toBe('La valeur est obligatoire');
        await app.close();
    });
    it('clears maxConcurrentReviews from persisted config when patch sends null', async () => {
        const gateway = new StubProjectConfigGateway();
        gateway.set('/repo/A', baseConfig({ maxConcurrentReviews: 4 }));
        const app = await buildAppWithPatch(gateway);
        const response = await app.inject({
            method: 'PATCH',
            url: '/api/project-config?path=' + encodeURIComponent('/repo/A'),
            payload: { maxConcurrentReviews: null },
        });
        expect(response.statusCode).toBe(200);
        expect(response.json().config.maxConcurrentReviews).toBeUndefined();
        await app.close();
    });
});
//# sourceMappingURL=projectConfig.routes.test.js.map