/**
 * Schema and Export Integration Tests
 * Verifies atlas listing, schema retrieval, and full export behavior through the
 * backend API using seeded data.
 */
const request = require('supertest');
const AdmZip = require('adm-zip');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');
const app = require('../../../server');

const testPool = new Pool({
    user: process.env.DB_USER || 'postgres',
    host: process.env.DB_HOST || 'localhost',
    database: process.env.DB_NAME || (process.env.NODE_ENV === 'test' ? 'postgres_test' : 'postgres'),
    password: process.env.DB_PASSWORD || 'betka',
    port: parseInt(process.env.DB_PORT || '5432', 10),
    allowExitOnIdle: true,
});
const USER_IMAGES_DIR = path.join(__dirname, '..', '..', '..', 'public', 'user_added_images');

const binaryParser = (res, callback) => {
    res.setEncoding('binary');
    let data = '';

    res.on('data', (chunk) => {
        data += chunk;
    });

    res.on('end', () => {
        callback(null, Buffer.from(data, 'binary'));
    });
};

const findFirstEditableOption = (schema) => {
    const trait = schema
        .flatMap((category) => category.znaky || [])
        .find((item) => Array.isArray(item.options) && item.options.length > 0);

    return trait ? { trait, option: trait.options[0] } : null;
};

const clearUserImageState = async () => {
    const tableExists = await testPool.query(`
        SELECT EXISTS (
            SELECT 1
            FROM information_schema.tables
            WHERE table_schema = 'public'
              AND table_name = 'user_contributed_images'
        ) AS exists
    `);

    if (tableExists.rows[0]?.exists) {
        await testPool.query('TRUNCATE TABLE user_contributed_images RESTART IDENTITY CASCADE');
    }

    if (!fs.existsSync(USER_IMAGES_DIR)) {
        fs.mkdirSync(USER_IMAGES_DIR, { recursive: true });
        return;
    }

    fs.readdirSync(USER_IMAGES_DIR).forEach((entry) => {
        const fullPath = path.join(USER_IMAGES_DIR, entry);
        if (fs.statSync(fullPath).isFile()) {
            fs.unlinkSync(fullPath);
        }
    });
};

describe('Atlases API - Comprehensive Tests', () => {
    describe('GET /api/atlases', () => {
        it('should return array of atlases', async () => {
            const res = await request(app).get('/api/atlases');
            expect(res.status).toBe(200);
            expect(Array.isArray(res.body)).toBe(true);
        });

        it('should return atlases with proper schema', async () => {
            const res = await request(app).get('/api/atlases');
            if (res.body.length > 0) {
                const atlas = res.body[0];
                expect(atlas).toHaveProperty('id');
                expect(typeof atlas.id).toBe('string');
                expect(atlas.id.length).toBeGreaterThan(0);
            }
        });

        it('should not return empty atlases', async () => {
            const res = await request(app).get('/api/atlases');
            expect(res.status).toBe(200);
            // Should have at least some atlases from seeding
            if (res.body.length > 0) {
                res.body.forEach(atlas => {
                    expect(atlas.id).toBeTruthy();
                });
            }
        });
    });
});

describe('Form Schema API - Comprehensive Tests', () => {
    describe('GET /api/form-schema', () => {
        it('should return schemas for all atlases when no parameter', async () => {
            const res = await request(app).get('/api/form-schema');
            expect(res.status).toBe(200);
            expect(Array.isArray(res.body)).toBe(true);
        });

        it('should return schema with proper structure', async () => {
            const res = await request(app).get('/api/form-schema');
            if (res.body.length > 0) {
                const item = res.body[0];
                expect(item).toHaveProperty('atlas_id');
            }
        });

        it('should handle invalid atlasId gracefully', async () => {
            const res = await request(app).get('/api/form-schema?atlasId=invalid_atlas_xyz');
            // Should either return empty or error
            expect([200, 500]).toContain(res.status);
        });

        it('should return schemas for specific atlas when atlasId provided', async () => {
            // First get available atlases
            const atlasRes = await request(app).get('/api/atlases');
            if (atlasRes.body.length > 0) {
                const atlasId = atlasRes.body[0].id;
                const schemaRes = await request(app).get(`/api/form-schema?atlasId=${atlasId}`);
                expect([200, 500]).toContain(schemaRes.status);
                if (schemaRes.status === 200) {
                    expect(Array.isArray(schemaRes.body)).toBe(true);
                }
            }
        });

        it('should include trait categories in schema', async () => {
            const res = await request(app).get('/api/form-schema');
            if (res.status === 200 && res.body.length > 0) {
                // Check structure for schema items
                res.body.forEach(item => {
                    if (item.id) {
                        // Has category structure
                        expect(typeof item.id).toBe('number');
                    }
                });
            }
        });

        it('should handle schema with complex trait structures', async () => {
            const res = await request(app).get('/api/form-schema');
            expect(res.status).toBe(200);
            
            if (res.body.length > 0) {
                const category = res.body[0];
                if (category.znaky) {
                    expect(Array.isArray(category.znaky)).toBe(true);
                    
                    category.znaky.forEach(trait => {
                        if (trait.options) {
                            expect(Array.isArray(trait.options)).toBe(true);
                            
                            trait.options.forEach(option => {
                                expect(option).toHaveProperty('id');
                                // Verify image arrays exist
                                if (option.factoryImages) {
                                    expect(Array.isArray(option.factoryImages)).toBe(true);
                                }
                                if (option.userImages) {
                                    expect(Array.isArray(option.userImages)).toBe(true);
                                }
                            });
                        }
                    });
                }
            }
        });
    });
});

describe('Export API - Comprehensive Tests', () => {
    beforeEach(async () => {
        await clearUserImageState();
    });

    afterAll(async () => {
        await clearUserImageState();
        await testPool.end();
    });

    describe('GET /export/full', () => {
        it('should return export data as array', async () => {
            const res = await request(app).get('/export/full');
            expect(res.status).toBe(200);
            expect(Array.isArray(res.body)).toBe(true);
        });

        it('should include required export fields', async () => {
            const res = await request(app).get('/export/full');
            if (res.body.length > 0) {
                const item = res.body[0];
                expect(item).toHaveProperty('annotationId');
                expect(item).toHaveProperty('atlasName');
                expect(item).toHaveProperty('values');
            }
        });

        it('should include checker name for checked annotations in export data', async () => {
            const uniqueName = `export-checker-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const annotatorName = 'Export Annotator';
            const checkerName = 'Export Corrector';

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

            const modelRes = await request(app)
                .post('/api/models')
                .field('modelId', uniqueName)
                .attach('modelFile', obj, 'export.obj')
                .attach('mtlFile', mtl, 'export.mtl')
                .attach('textureFile', texture, 'export.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 values = { [editableTrait.id]: editableTrait.options[0].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 checkRes = await request(app)
                    .patch(`/api/annotations/${annotationId}/check`)
                    .send({ isChecked: true, checkerName });

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

                const exportRes = await request(app).get('/export/full');
                expect(exportRes.status).toBe(200);
                const exportedItem = exportRes.body.find(item => item.annotationId === annotationId);

                expect(exportedItem).toBeDefined();
                expect(exportedItem).toEqual(expect.objectContaining({
                    annotationId,
                    annotator: annotatorName,
                    isChecked: true,
                    checkerName,
                }));
            } finally {
                if (createdModelId) {
                    await request(app).delete(`/api/models/${createdModelId}`);
                }
            }
        });

        it('should handle empty export data', async () => {
            const res = await request(app).get('/export/full');
            expect(res.status).toBe(200);
            expect(Array.isArray(res.body)).toBe(true);
            // May be empty if no annotations
        });
    });

    describe('GET /api/updates/export', () => {
        it('should handle no user updates gracefully', async () => {
            const res = await request(app).get('/api/updates/export');
            // Should either return empty or 404
            expect([200, 404]).toContain(res.status);
        });

        it('should return proper error when no updates exist', async () => {
            const res = await request(app).get('/api/updates/export');
            if (res.status === 404) {
                expect(res.body).toHaveProperty('error');
            }
        });

        it('should export deduped updates and stop exporting them after delete', async () => {
            const atlasRes = await request(app).get('/api/atlases');
            expect(atlasRes.status).toBe(200);
            const atlasId = atlasRes.body[0]?.id;
            expect(atlasId).toBeTruthy();

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

            const uniqueFileName = `atlas-test-${Date.now()}.png`;
            const imageBufferA = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x01]);
            const imageBufferB = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x02]);

            const firstUploadRes = await request(app)
                .post('/api/atlases/add-image')
                .field('traitId', editable.trait.id)
                .field('optionLabelCs', editable.option.label_cs)
                .field('isDrawing', 'false')
                .attach('image', imageBufferA, uniqueFileName);

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

            const secondUploadRes = await request(app)
                .post('/api/atlases/add-image')
                .field('traitId', editable.trait.id)
                .field('optionLabelCs', editable.option.label_cs)
                .field('isDrawing', 'false')
                .attach('image', imageBufferB, uniqueFileName);

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

            const updatedSchemaRes = await request(app).get(`/api/form-schema?atlasId=${atlasId}`);
            expect(updatedSchemaRes.status).toBe(200);
            const updatedEditable = findFirstEditableOption(updatedSchemaRes.body);
            const matchingOption = updatedEditable.trait.options.find((option) => option.id === editable.option.id);
            const matchingUserImages = (matchingOption.userImages || []).filter((image) => image.path.includes('/public/user_added_images/'));

            expect(matchingUserImages).toHaveLength(1);

            const exportRes = await request(app)
                .get('/api/updates/export')
                .buffer(true)
                .parse(binaryParser);

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

            const zip = new AdmZip(exportRes.body);
            const updatesEntry = zip.getEntry('updates.json');
            expect(updatesEntry).toBeTruthy();

            const updates = JSON.parse(zip.readAsText(updatesEntry));
            const matchingUpdates = updates.filter((update) => update.original_filename === uniqueFileName);

            expect(matchingUpdates).toHaveLength(1);

            const imageEntries = zip.getEntries().filter((entry) => entry.entryName.startsWith('images/'));
            expect(imageEntries.filter((entry) => entry.entryName === `images/${matchingUpdates[0].image_path.split('/').pop()}`)).toHaveLength(1);

            const deleteRes = await request(app).delete(`/api/atlases/user-image/${matchingUserImages[0].id}`);
            expect(deleteRes.status).toBe(200);

            const schemaAfterDeleteRes = await request(app).get(`/api/form-schema?atlasId=${atlasId}`);
            expect(schemaAfterDeleteRes.status).toBe(200);
            const editableAfterDelete = findFirstEditableOption(schemaAfterDeleteRes.body);
            const optionAfterDelete = editableAfterDelete.trait.options.find((option) => option.id === editable.option.id);
            const remainingMatches = (optionAfterDelete.userImages || []).filter((image) => image.path.includes('/public/user_added_images/'));
            expect(remainingMatches).toHaveLength(0);

            const exportAfterDeleteRes = await request(app).get('/api/updates/export');

            if (exportAfterDeleteRes.status === 404) {
                expect(exportAfterDeleteRes.body.error).toBeTruthy();
                return;
            }

            expect(exportAfterDeleteRes.status).toBe(200);
            const exportZipAfterDeleteRes = await request(app)
                .get('/api/updates/export')
                .buffer(true)
                .parse(binaryParser);
            const zipAfterDelete = new AdmZip(exportZipAfterDeleteRes.body);
            const updatesAfterDelete = JSON.parse(zipAfterDelete.readAsText(zipAfterDelete.getEntry('updates.json')));
            expect(updatesAfterDelete.filter((update) => update.original_filename === uniqueFileName)).toHaveLength(0);
        });
    });

    describe('POST /api/updates/import', () => {
        it('should reject import without file', async () => {
            const res = await request(app).post('/api/updates/import');
            expect(res.status).toBe(400);
            expect(res.body).toHaveProperty('error');
        });

        it('should reject invalid zip file', async () => {
            const res = await request(app)
                .post('/api/updates/import')
                .attach('updateZip', Buffer.from('not a zip file'), 'invalid.zip');
            expect(res.status).toBe(500);
        });

        it('should report added and duplicate image counts correctly', async () => {
            const atlasRes = await request(app).get('/api/atlases');
            expect(atlasRes.status).toBe(200);
            const atlasId = atlasRes.body[0]?.id;
            expect(atlasId).toBeTruthy();

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

            const zipFileName = `import-counts-${Date.now()}.png`;
            const zip = new AdmZip();
            zip.addFile('updates.json', Buffer.from(JSON.stringify([
                {
                    image_path: `/public/user_added_images/${zipFileName}`,
                    is_drawing: false,
                    original_filename: 'import-counts-original.png',
                    trait_id: editable.trait.id,
                    option_label_cs: editable.option.label_cs,
                },
            ], null, 2)));
            zip.addFile(`images/${zipFileName}`, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x05]));
            const zipBuffer = zip.toBuffer();

            const firstImportRes = await request(app)
                .post('/api/updates/import')
                .attach('updateZip', zipBuffer, 'updates.zip');

            expect(firstImportRes.status).toBe(200);
            expect(firstImportRes.body).toEqual(expect.objectContaining({
                addedCount: 1,
                duplicateCount: 0,
            }));

            const secondImportRes = await request(app)
                .post('/api/updates/import')
                .attach('updateZip', zipBuffer, 'updates.zip');

            expect(secondImportRes.status).toBe(200);
            expect(secondImportRes.body).toEqual(expect.objectContaining({
                addedCount: 0,
                duplicateCount: 1,
            }));
        });
    });
});