/**
 * Annotations API Integration Tests
 * Covers annotation listing, detail loading, save flows, edit workflows, and
 * checked-state behavior through the live Express application.
 */
const request = require('supertest');
const crypto = require('crypto');
const app = require('../../../server');

describe('Annotations API - Comprehensive Tests', () => {
    describe('GET /annotations', () => {
        it('should return annotations with proper structure', async () => {
            const res = await request(app).get('/annotations');
            expect(res.status).toBe(200);
            expect(res.body).toHaveProperty('annotations');
            
            if (res.body.annotations.length > 0) {
                const ann = res.body.annotations[0];
                expect(ann).toHaveProperty('annotationId');
                expect(ann).toHaveProperty('atlasId');
                expect(ann).toHaveProperty('annotator');
                expect(ann).toHaveProperty('lastUpdated');
            }
        });

        it('should handle large result sets', async () => {
            const res = await request(app).get('/annotations');
            expect(res.status).toBe(200);
            expect(Array.isArray(res.body.annotations)).toBe(true);
        });
    });

    describe('GET /annotations/:id', () => {
        it('should return 400 for invalid uuid format', async () => {
            const res = await request(app).get('/annotations/not-a-uuid');
            expect(res.status).toBe(400);
        });

        it('should return 404 for non-existent valid uuid', async () => {
            const validUuid = '123e4567-e89b-12d3-a456-426614174000';
            const res = await request(app).get(`/annotations/${validUuid}`);
            expect(res.status).toBe(404);
        });
    });

    describe('POST /annotations', () => {
        it('should reject empty body', async () => {
            const res = await request(app).post('/annotations').send({});
            expect(res.status).toBeGreaterThanOrEqual(400);
        });

        it('should preserve the original foreign annotation and create a new one for a different annotator edit', async () => {
            const uniqueName = `fork-workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const originalAnnotator = 'Original Annotator';
            const editingAnnotator = 'Current Annotator';

            const obj = Buffer.from('mtllib fork.mtl\nv 0 0 0\nf 1 1 1\n');
            const mtl = Buffer.from('newmtl mat\nmap_Kd fork.jpg\n');
            const texture = Buffer.from([0xff, 0xd8, 0xff, 0xd9, 0x02]);
            const fileHash = crypto.createHash('sha256').update(obj).digest('hex');

            const modelRes = await request(app)
                .post('/api/models')
                .field('modelId', uniqueName)
                .attach('modelFile', obj, 'fork.obj')
                .attach('mtlFile', mtl, 'fork.mtl')
                .attach('textureFile', texture, 'fork.jpg');

            expect(modelRes.status).toBe(201);
            const createdModelId = modelRes.body?.modelId;
            expect(createdModelId).toBeTruthy();

            try {
                const atlasesRes = await request(app).get('/api/atlases');
                expect(atlasesRes.status).toBe(200);
                const atlasId = atlasesRes.body[0]?.id;
                expect(atlasId).toBeTruthy();

                const schemaRes = await request(app).get(`/api/form-schema?atlasId=${atlasId}`);
                expect(schemaRes.status).toBe(200);

                const editableTrait = schemaRes.body
                    .flatMap(category => category.znaky || [])
                    .find(trait => Array.isArray(trait.options) && trait.options.length >= 2);

                expect(editableTrait).toBeTruthy();
                const [originalOption, editedOption] = editableTrait.options;

                const originalValues = { [editableTrait.id]: originalOption.label_cs };
                const editedValues = { [editableTrait.id]: editedOption.label_cs };

                const originalSaveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', atlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', originalAnnotator)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(originalValues));

                expect(originalSaveRes.status).toBe(200);
                const originalAnnotationId = originalSaveRes.body?.annotationId;
                expect(originalAnnotationId).toBeTruthy();

                const forkedSaveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', atlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', editingAnnotator)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(editedValues));

                expect(forkedSaveRes.status).toBe(200);
                const forkedAnnotationId = forkedSaveRes.body?.annotationId;
                expect(forkedAnnotationId).toBeTruthy();
                expect(forkedAnnotationId).not.toBe(originalAnnotationId);

                const annotationsRes = await request(app).get('/annotations');
                expect(annotationsRes.status).toBe(200);

                const matchingAnnotations = (annotationsRes.body.annotations || []).filter(annotation =>
                    annotation.fileHash === fileHash && annotation.atlasId === atlasId
                );

                expect(matchingAnnotations).toEqual(
                    expect.arrayContaining([
                        expect.objectContaining({
                            annotationId: originalAnnotationId,
                            annotator: originalAnnotator,
                        }),
                        expect.objectContaining({
                            annotationId: forkedAnnotationId,
                            annotator: editingAnnotator,
                        }),
                    ])
                );

                const originalDetailRes = await request(app).get(`/annotations/${originalAnnotationId}`);
                expect(originalDetailRes.status).toBe(200);
                expect(originalDetailRes.body.annotator).toBe(originalAnnotator);
                expect(originalDetailRes.body.values).toMatchObject(originalValues);

                const forkedDetailRes = await request(app).get(`/annotations/${forkedAnnotationId}`);
                expect(forkedDetailRes.status).toBe(200);
                expect(forkedDetailRes.body.annotator).toBe(editingAnnotator);
                expect(forkedDetailRes.body.values).toMatchObject(editedValues);
            } finally {
                if (createdModelId) {
                    await request(app).delete(`/api/models/${createdModelId}`);
                }
            }
        });

        it('should update check status without creating a forked annotation', async () => {
            const uniqueName = `check-workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const annotatorName = 'Checked Annotator';
            const checkerName = 'Reviewing Corrector';

            const obj = Buffer.from('mtllib check.mtl\nv 0 0 0\nf 1 1 1\n');
            const mtl = Buffer.from('newmtl mat\nmap_Kd check.jpg\n');
            const texture = Buffer.from([0xff, 0xd8, 0xff, 0xd9, 0x03]);
            const fileHash = crypto.createHash('sha256').update(obj).digest('hex');

            const modelRes = await request(app)
                .post('/api/models')
                .field('modelId', uniqueName)
                .attach('modelFile', obj, 'check.obj')
                .attach('mtlFile', mtl, 'check.mtl')
                .attach('textureFile', texture, 'check.jpg');

            expect(modelRes.status).toBe(201);
            const createdModelId = modelRes.body?.modelId;
            expect(createdModelId).toBeTruthy();

            try {
                const atlasesRes = await request(app).get('/api/atlases');
                expect(atlasesRes.status).toBe(200);
                const atlasId = atlasesRes.body[0]?.id;
                expect(atlasId).toBeTruthy();

                const schemaRes = await request(app).get(`/api/form-schema?atlasId=${atlasId}`);
                expect(schemaRes.status).toBe(200);

                const editableTrait = schemaRes.body
                    .flatMap(category => category.znaky || [])
                    .find(trait => Array.isArray(trait.options) && trait.options.length > 0);

                expect(editableTrait).toBeTruthy();
                const selectedOption = editableTrait.options[0];
                const values = { [editableTrait.id]: selectedOption.label_cs };

                const saveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', atlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', annotatorName)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(values));

                expect(saveRes.status).toBe(200);
                const annotationId = saveRes.body?.annotationId;
                expect(annotationId).toBeTruthy();

                const beforeCheckRes = await request(app).get('/annotations');
                expect(beforeCheckRes.status).toBe(200);
                const matchingBeforeCheck = (beforeCheckRes.body.annotations || []).filter(annotation =>
                    annotation.fileHash === fileHash && annotation.atlasId === atlasId
                );
                expect(matchingBeforeCheck).toHaveLength(1);

                const checkRes = await request(app)
                    .patch(`/api/annotations/${annotationId}/check`)
                    .send({ isChecked: true, checkerName });

                expect(checkRes.status).toBe(200);

                const afterCheckRes = await request(app).get('/annotations');
                expect(afterCheckRes.status).toBe(200);
                const matchingAfterCheck = (afterCheckRes.body.annotations || []).filter(annotation =>
                    annotation.fileHash === fileHash && annotation.atlasId === atlasId
                );

                expect(matchingAfterCheck).toHaveLength(1);
                expect(matchingAfterCheck[0]).toEqual(expect.objectContaining({
                    annotationId,
                    annotator: annotatorName,
                    isChecked: true,
                    checkerName,
                }));

                const detailRes = await request(app).get(`/annotations/${annotationId}`);
                expect(detailRes.status).toBe(200);
                expect(detailRes.body.annotator).toBe(annotatorName);
                expect(detailRes.body.checkerName).toBe(checkerName);
                expect(detailRes.body.isChecked).toBe(true);
                expect(detailRes.body.values).toMatchObject(values);
            } finally {
                if (createdModelId) {
                    await request(app).delete(`/api/models/${createdModelId}`);
                }
            }
        });

        it('should update the existing annotation in place when the same annotator saves edits', async () => {
            const uniqueName = `same-annotator-update-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const annotatorName = 'Same Annotator';

            const obj = Buffer.from('mtllib same.mtl\nv 0 0 0\nf 1 1 1\n');
            const mtl = Buffer.from('newmtl mat\nmap_Kd same.jpg\n');
            const texture = Buffer.from([0xff, 0xd8, 0xff, 0xd9, 0x04]);
            const fileHash = crypto.createHash('sha256').update(obj).digest('hex');

            const modelRes = await request(app)
                .post('/api/models')
                .field('modelId', uniqueName)
                .attach('modelFile', obj, 'same.obj')
                .attach('mtlFile', mtl, 'same.mtl')
                .attach('textureFile', texture, 'same.jpg');

            expect(modelRes.status).toBe(201);
            const createdModelId = modelRes.body?.modelId;
            expect(createdModelId).toBeTruthy();

            try {
                const atlasesRes = await request(app).get('/api/atlases');
                expect(atlasesRes.status).toBe(200);
                const atlasId = atlasesRes.body[0]?.id;
                expect(atlasId).toBeTruthy();

                const schemaRes = await request(app).get(`/api/form-schema?atlasId=${atlasId}`);
                expect(schemaRes.status).toBe(200);

                const editableTrait = schemaRes.body
                    .flatMap(category => category.znaky || [])
                    .find(trait => Array.isArray(trait.options) && trait.options.length >= 2);

                expect(editableTrait).toBeTruthy();
                const [initialOption, updatedOption] = editableTrait.options;
                const initialValues = { [editableTrait.id]: initialOption.label_cs };
                const updatedValues = { [editableTrait.id]: updatedOption.label_cs };

                const firstSaveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', atlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', annotatorName)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(initialValues));

                expect(firstSaveRes.status).toBe(200);
                const firstAnnotationId = firstSaveRes.body?.annotationId;
                expect(firstAnnotationId).toBeTruthy();

                const secondSaveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', atlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', annotatorName)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(updatedValues));

                expect(secondSaveRes.status).toBe(200);
                const secondAnnotationId = secondSaveRes.body?.annotationId;
                expect(secondAnnotationId).toBe(firstAnnotationId);

                const annotationsRes = await request(app).get('/annotations');
                expect(annotationsRes.status).toBe(200);

                const matchingAnnotations = (annotationsRes.body.annotations || []).filter(annotation =>
                    annotation.fileHash === fileHash &&
                    annotation.atlasId === atlasId &&
                    annotation.annotator === annotatorName
                );

                expect(matchingAnnotations).toHaveLength(1);
                expect(matchingAnnotations[0]).toEqual(expect.objectContaining({
                    annotationId: firstAnnotationId,
                    annotator: annotatorName,
                }));

                const detailRes = await request(app).get(`/annotations/${firstAnnotationId}`);
                expect(detailRes.status).toBe(200);
                expect(detailRes.body.values).toMatchObject(updatedValues);
                expect(detailRes.body.values).not.toMatchObject(initialValues);
            } finally {
                if (createdModelId) {
                    await request(app).delete(`/api/models/${createdModelId}`);
                }
            }
        });

        it('should create a separate annotation when the same annotator saves the same model under a different atlas', async () => {
            const uniqueName = `same-annotator-different-atlas-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const annotatorName = 'Atlas Switching Annotator';

            const obj = Buffer.from('mtllib atlas.mtl\nv 0 0 0\nf 1 1 1\n');
            const mtl = Buffer.from('newmtl mat\nmap_Kd atlas.jpg\n');
            const texture = Buffer.from([0xff, 0xd8, 0xff, 0xd9, 0x05]);
            const fileHash = crypto.createHash('sha256').update(obj).digest('hex');

            const modelRes = await request(app)
                .post('/api/models')
                .field('modelId', uniqueName)
                .attach('modelFile', obj, 'atlas.obj')
                .attach('mtlFile', mtl, 'atlas.mtl')
                .attach('textureFile', texture, 'atlas.jpg');

            expect(modelRes.status).toBe(201);
            const createdModelId = modelRes.body?.modelId;
            expect(createdModelId).toBeTruthy();

            try {
                const atlasesRes = await request(app).get('/api/atlases');
                expect(atlasesRes.status).toBe(200);
                expect(Array.isArray(atlasesRes.body)).toBe(true);
                expect(atlasesRes.body.length).toBeGreaterThanOrEqual(2);

                const [firstAtlas, secondAtlas] = atlasesRes.body;
                const firstAtlasId = firstAtlas.id;
                const secondAtlasId = secondAtlas.id;

                const firstSchemaRes = await request(app).get(`/api/form-schema?atlasId=${firstAtlasId}`);
                expect(firstSchemaRes.status).toBe(200);
                const firstTrait = firstSchemaRes.body
                    .flatMap(category => category.znaky || [])
                    .find(trait => Array.isArray(trait.options) && trait.options.length > 0);
                expect(firstTrait).toBeTruthy();

                const secondSchemaRes = await request(app).get(`/api/form-schema?atlasId=${secondAtlasId}`);
                expect(secondSchemaRes.status).toBe(200);
                const secondTrait = secondSchemaRes.body
                    .flatMap(category => category.znaky || [])
                    .find(trait => Array.isArray(trait.options) && trait.options.length > 0);
                expect(secondTrait).toBeTruthy();

                const firstValues = { [firstTrait.id]: firstTrait.options[0].label_cs };
                const secondValues = { [secondTrait.id]: secondTrait.options[0].label_cs };

                const firstSaveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', firstAtlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', annotatorName)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(firstValues));

                expect(firstSaveRes.status).toBe(200);
                const firstAnnotationId = firstSaveRes.body?.annotationId;
                expect(firstAnnotationId).toBeTruthy();

                const secondSaveRes = await request(app)
                    .post('/annotations')
                    .field('atlasId', secondAtlasId)
                    .field('modelId', uniqueName)
                    .field('annotatorName', annotatorName)
                    .field('fileHash', fileHash)
                    .field('values', JSON.stringify(secondValues));

                expect(secondSaveRes.status).toBe(200);
                const secondAnnotationId = secondSaveRes.body?.annotationId;
                expect(secondAnnotationId).toBeTruthy();
                expect(secondAnnotationId).not.toBe(firstAnnotationId);

                const annotationsRes = await request(app).get('/annotations');
                expect(annotationsRes.status).toBe(200);

                const matchingAnnotations = (annotationsRes.body.annotations || []).filter(annotation =>
                    annotation.fileHash === fileHash && annotation.annotator === annotatorName
                );

                expect(matchingAnnotations).toEqual(
                    expect.arrayContaining([
                        expect.objectContaining({
                            annotationId: firstAnnotationId,
                            atlasId: firstAtlasId,
                            annotator: annotatorName,
                        }),
                        expect.objectContaining({
                            annotationId: secondAnnotationId,
                            atlasId: secondAtlasId,
                            annotator: annotatorName,
                        }),
                    ])
                );

                const firstDetailRes = await request(app).get(`/annotations/${firstAnnotationId}`);
                expect(firstDetailRes.status).toBe(200);
                expect(firstDetailRes.body.atlasId).toBe(firstAtlasId);
                expect(firstDetailRes.body.values).toMatchObject(firstValues);

                const secondDetailRes = await request(app).get(`/annotations/${secondAnnotationId}`);
                expect(secondDetailRes.status).toBe(200);
                expect(secondDetailRes.body.atlasId).toBe(secondAtlasId);
                expect(secondDetailRes.body.values).toMatchObject(secondValues);
            } finally {
                if (createdModelId) {
                    await request(app).delete(`/api/models/${createdModelId}`);
                }
            }
        });

        it('should reject missing atlasId', async () => {
            const res = await request(app)
                .post('/annotations')
                .field('fileHash', 'hash123')
                .field('annotatorName', 'User');
            expect(res.status).toBeGreaterThanOrEqual(400);
        });

        it('should reject missing annotatorName', async () => {
            const res = await request(app)
                .post('/annotations')
                .field('atlasId', 'atlas-1')
                .field('fileHash', 'hash123');
            expect(res.status).toBeGreaterThanOrEqual(400);
        });

        it('should reject missing fileHash', async () => {
            const res = await request(app)
                .post('/annotations')
                .field('atlasId', 'atlas-1')
                .field('annotatorName', 'User');
            expect(res.status).toBeGreaterThanOrEqual(400);
        });

        it('should reject missing values', async () => {
            const res = await request(app)
                .post('/annotations')
                .field('atlasId', 'atlas-1')
                .field('fileHash', 'hash123')
                .field('annotatorName', 'User');
            expect(res.status).toBeGreaterThanOrEqual(400);
        });
    });

    describe('DELETE /annotations/:id', () => {
        it('should handle non-existent annotations gracefully', async () => {
            const validUuid = '123e4567-e89b-12d3-a456-426614174000';
            const res = await request(app).delete(`/annotations/${validUuid}`);
            // Should be idempotent - either 200 or 404 is acceptable
            expect([200, 404]).toContain(res.status);
        });

        it('should reject invalid uuid format', async () => {
            const res = await request(app).delete('/annotations/invalid-id');
            expect(res.status).toBeGreaterThanOrEqual(400);
        });
    });

    describe('PATCH /api/annotations/:id/check', () => {
        it('should update check status', async () => {
            const res = await request(app)
                .patch('/api/annotations/123e4567-e89b-12d3-a456-426614174000/check')
                .send({ isChecked: true });
            // Should handle gracefully even if annotation doesn't exist
            expect([200, 400, 404, 500]).toContain(res.status);
        });

        it('should accept boolean values for isChecked', async () => {
            const uuid = '123e4567-e89b-12d3-a456-426614174000';
            const res1 = await request(app)
                .patch(`/api/annotations/${uuid}/check`)
                .send({ isChecked: true });
            expect([200, 404, 500]).toContain(res1.status);

            const res2 = await request(app)
                .patch(`/api/annotations/${uuid}/check`)
                .send({ isChecked: false });
            expect([200, 404, 500]).toContain(res2.status);
        });
    });
});