const request = require('supertest');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
const { Pool } = require('pg');
const app = require('../../../server');

const BACKEND_DIR = path.join(__dirname, '..', '..', '..');
const UPLOADS_DIR = path.join(BACKEND_DIR, 'uploads');
const USER_IMAGES_DIR = path.join(BACKEND_DIR, 'public', 'user_added_images');

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,
});

describe('Critical backend workflows', () => {
  afterAll(async () => {
    await testPool.end();
  });

  describe('Model duplicate content detection', () => {
    it('rejects identical OBJ content with different filenames and removes temporary upload files', async () => {
      const beforeCount = fs.readdirSync(UPLOADS_DIR).length;
      const obj = Buffer.from('mtllib duplicate.mtl\nv 0 0 0\nf 1 1 1\n');
      const mtl = Buffer.from('newmtl mat\nmap_Kd duplicate.jpg\n');
      const texture = Buffer.from([0xff, 0xd8, 0xff, 0xd9]);
      const fileHash = crypto.createHash('sha256').update(obj).digest('hex');
      const uniqueName = `duplicate-content-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

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

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

      const afterFirstCount = fs.readdirSync(UPLOADS_DIR).length;
      expect(afterFirstCount).toBe(beforeCount + 3);

      const duplicateRes = await request(app)
        .post('/api/models')
        .field('modelId', `${uniqueName}-other`) 
        .attach('modelFile', obj, 'duplicate.obj')
        .attach('mtlFile', mtl, 'duplicate.mtl')
        .attach('textureFile', texture, 'duplicate.jpg');

      expect(duplicateRes.status).toBe(409);
      expect(duplicateRes.body.errorCode).toBe('modelContentAlreadyExists');
      expect(duplicateRes.body.existingModelId).toBe(createdModelId);

      const afterDuplicateCount = fs.readdirSync(UPLOADS_DIR).length;
      expect(afterDuplicateCount).toBe(afterFirstCount);

      const modelsRes = await request(app).get('/api/models');
      expect(modelsRes.status).toBe(200);
      const matchingModels = modelsRes.body.filter((item) => item.file_hash === fileHash);
      expect(matchingModels).toHaveLength(1);
      expect(matchingModels[0].external_id).toBe(uniqueName);

      const deleteRes = await request(app).delete(`/api/models/${createdModelId}`);
      expect(deleteRes.status).toBe(200);
      expect(fs.readdirSync(UPLOADS_DIR).length).toBe(beforeCount);
    });
  });

  describe('ZIP import rollback', () => {
    it('rolls back all changes when a ZIP import contains corrupted metadata', async () => {
      const initialCount = parseInt((await testPool.query('SELECT COUNT(*) AS count FROM user_contributed_images')).rows[0].count, 10);
      const traitRow = (await testPool.query('SELECT trait_id, label_cs FROM trait_options LIMIT 1')).rows[0];
      expect(traitRow).toBeDefined();

      const zip = new AdmZip();
      const validUpdate = {
        image_path: '/public/user_added_images/rollback-valid.png',
        is_drawing: false,
        original_filename: 'rollback-valid.png',
        trait_id: traitRow.trait_id,
        option_label_cs: traitRow.label_cs,
      };

      const corruptedMetadata = `[
${JSON.stringify(validUpdate)},
{"image_path":"/public/user_added_images/rollback-broken.png","is_drawing":false,"original_filename":"rollback-broken.png","trait_id":${JSON.stringify(traitRow.trait_id)},"option_label_cs":`;
      zip.addFile('updates.json', Buffer.from(corruptedMetadata));
      zip.addFile('images/rollback-valid.png', Buffer.from([0x89, 0x50, 0x4e, 0x47]));

      const res = await request(app)
        .post('/api/updates/import')
        .attach('updateZip', zip.toBuffer(), 'rollback.zip');

      expect(res.status).toBe(500);
      expect(res.body.error).toMatch(/Unexpected token|JSON/);

      const afterCount = parseInt((await testPool.query('SELECT COUNT(*) AS count FROM user_contributed_images')).rows[0].count, 10);
      expect(afterCount).toBe(initialCount);
      expect(fs.existsSync(path.join(USER_IMAGES_DIR, 'rollback-valid.png'))).toBe(false);
      expect(fs.existsSync(path.join(USER_IMAGES_DIR, 'rollback-broken.png'))).toBe(false);
    });
  });

  describe('Annotation checked state lifecycle', () => {
    it('clears checkerName when an annotation is unchecked after checking', async () => {
      const obj = Buffer.from('mtllib check-state.mtl\nv 0 0 0\nf 1 1 1\n');
      const mtl = Buffer.from('newmtl mat\nmap_Kd check-state.jpg\n');
      const texture = Buffer.from([0xff, 0xd8, 0xff, 0xd9]);
      const fileHash = crypto.createHash('sha256').update(obj).digest('hex');
      const uniqueName = `check-lifecycle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

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

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

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

      const saveRes = await request(app)
        .post('/annotations')
        .field('atlasId', atlasId)
        .field('modelId', uniqueName)
        .field('fileHash', fileHash)
        .field('annotatorName', 'Checker User')
        .field('checkerName', 'Original Checker')
        .field('isChecked', 'true')
        .field('values', JSON.stringify({}));

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

      const detailRes = await request(app).get(`/annotations/${annotationId}`);
      expect(detailRes.status).toBe(200);
      expect(detailRes.body.isChecked).toBe(true);
      expect(detailRes.body.checkerName).toBe('Original Checker');

      const uncheckRes = await request(app)
        .patch(`/api/annotations/${annotationId}/check`)
        .send({ isChecked: false });

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

      const detailAfterRes = await request(app).get(`/annotations/${annotationId}`);
      expect(detailAfterRes.status).toBe(200);
      expect(detailAfterRes.body.isChecked).toBe(false);
      expect(detailAfterRes.body.checkerName).toBeNull();

      const deleteModelRes = await request(app).delete(`/api/models/${createdModelId}`);
      expect(deleteModelRes.status).toBe(200);
    });
  });
});
