// Tests for the Legacy Import Controller (FR-SMS3).
//
// Validates the three core routes:
//   POST /api/legacy-import/preview
//   POST /api/legacy-import/commit
//   POST /api/legacy-import/error-csv
//   GET  /api/legacy-import/mappings
//
// We seed two CSV files into the uploads dir and exercise the full
// preview -> commit cycle, including the rollback-on-error path.

const { pointAtDevDb } = require('./setup');
const path = require('path');
const fs = require('fs');

pointAtDevDb();
const request = require('supertest');
const app = require('../src/index');

const TMP = path.join(__dirname, '__tmp_legacy_imports');
if (!fs.existsSync(TMP)) fs.mkdirSync(TMP, { recursive: true });

const writeCsv = (name, body) => {
  const p = path.join(TMP, name);
  fs.writeFileSync(p, body, 'utf8');
  return p;
};

describe('Legacy Import Controller (FR-SMS3)', () => {
  let adminToken;
  let studentToken;
  let createdStudentEmails = []; // for cleanup

  beforeAll(async () => {
    const r1 = await request(app).post('/api/auth/login').send({ email: 'admin@school.com', password: 'admin123' });
    adminToken = r1.body?.token;
    const r2 = await request(app).post('/api/auth/login').send({ email: 'student@school.com', password: 'student123' });
    studentToken = r2.body?.token;
  });

  // ==================== Preview ====================

  describe('POST /api/legacy-import/preview', () => {
    it('auto-detects column mapping from a well-formed CSV', async () => {
      const csv = [
        'First Name,Last Name,Email,Phone,Class,Section,Date of Birth,Gender',
        'TestOne,LegacyOne,testone.legacy1@example.test,+263111111,Form 1,A,2010-01-01,Male',
        'TestTwo,LegacyTwo,testtwo.legacy2@example.test,+263222222,Form 2,B,2011-02-02,Female',
      ].join('\n');
      const p = writeCsv('preview-happy.csv', csv);

      const res = await request(app)
        .post('/api/legacy-import/preview')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p);

      expect(res.status).toBe(200);
      expect(res.body.total_rows).toBe(2);
      expect(res.body.valid_rows).toBe(2);
      expect(res.body.invalid_rows).toBe(0);
      expect(res.body.detected_headers).toEqual(expect.arrayContaining([
        'First Name', 'Last Name', 'Email', 'Phone', 'Class', 'Section', 'Date of Birth', 'Gender',
      ]));
      // Auto-detected mapping should pick up our aliases
      expect(res.body.detected_mapping['First Name']).toBe('first_name');
      expect(res.body.detected_mapping['Last Name']).toBe('last_name');
      expect(res.body.detected_mapping['Email']).toBe('email');
      expect(res.body.detected_mapping['Class']).toBe('class_name');
      expect(res.body.detected_mapping['Date of Birth']).toBe('date_of_birth');
      expect(res.body.detected_mapping['Gender']).toBe('gender');
    });

    it('returns validation errors for missing required fields', async () => {
      const csv = [
        'First Name,Last Name,Email',
        ',MissingLast,only.first@example.test',
      ].join('\n');
      const p = writeCsv('preview-missing.csv', csv);

      const res = await request(app)
        .post('/api/legacy-import/preview')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p);

      expect(res.status).toBe(200);
      expect(res.body.total_rows).toBe(1);
      expect(res.body.invalid_rows).toBe(1);
      // Row 2 has first_name='' (empty), last_name='MissingLast', email valid.
      // So only `missing first_name` is in the error list.
      expect(res.body.preview_rows[0].errors).toEqual(['missing first_name']);
    });

    it('returns 403 for a student (operations admin only)', async () => {
      // The role gate fires before multer touches the multipart body, so
      // a real file isn't needed here — the test is about RBAC, not
      // upload handling. Sending a file triggers an ECONNRESET on the
      // supertest side because the server closes the connection mid
      // chunked upload, which is a transport quirk, not a logic issue.
      const res = await request(app)
        .post('/api/legacy-import/preview')
        .set('Authorization', `Bearer ${studentToken}`)
        .send({});
      expect(res.status).toBe(403);
    });

    it('returns 401 without a token', async () => {
      // Send a plain JSON body (no file) — the auth middleware should
      // short-circuit before multer touches the request, so we don't
      // need to send a multipart payload.
      const res = await request(app)
        .post('/api/legacy-import/preview')
        .set('Content-Type', 'application/json')
        .send({});
      expect(res.status).toBe(401);
    });

    it('rejects a non-CSV file extension with a 400', async () => {
      const p = path.join(TMP, 'preview-bad.txt');
      fs.writeFileSync(p, 'hello', 'utf8');
      const res = await request(app)
        .post('/api/legacy-import/preview')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p);
      // handleUpload wrapper converts the multer fileFilter error to 400
      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/csv/i);
    });
  });

  // ==================== Commit ====================

  describe('POST /api/legacy-import/commit', () => {
    it('creates students, classes, enrollments, and parent links (rollback-on-error default)', async () => {
      // Unique suffix per test run so we don't collide with the same email across re-runs.
      const stamp = Date.now();
      const email1 = `legacy.commit.${stamp}.a@example.test`;
      const email2 = `legacy.commit.${stamp}.b@example.test`;
      const parentEmail = `legacy.parent.${stamp}@example.test`;
      // Seed a parent user in the DB so the legacy import can link to them
      // (the legacy controller only LINKS existing parents — it does not
      // create parent users from parent_email).
      const Database = require('better-sqlite3');
      const db = new Database(process.env.DB_PATH);
      const parentUid = `legacy-parent-${stamp}`;
      const bcrypt = require('bcryptjs');
      const hash = bcrypt.hashSync('parent-test-pw', 4);
      db.prepare(`
        INSERT INTO users (uid, email, role, first_name, last_name, is_active, password, sync_status)
        VALUES (?, ?, 'parent', 'Parent', 'Legacy', 1, ?, 'pending')
      `).run(parentUid, parentEmail, hash);
      db.close();
      createdStudentEmails.push(email1, email2, parentEmail);

      const csv = [
        'first_name,last_name,email,phone,class_name,section,date_of_birth,gender,parent_email,parent_name',
        `LegacyA,CommitA,${email1},+263777000001,Form 9A-${stamp},A,2010-05-15,Male,${parentEmail},Parent Legacy`,
        `LegacyB,CommitB,${email2},+263777000002,Form 9B-${stamp},B,2011-06-16,Female,${parentEmail},Parent Legacy`,
      ].join('\n');
      const p = writeCsv('commit-happy.csv', csv);

      const res = await request(app)
        .post('/api/legacy-import/commit')
        .set('Authorization', `Bearer ${adminToken}`)
        .field('mapping_name', `e2e-test-mapping-${stamp}`)
        .field('academic_year', '2026')
        .field('term', 'Term 1')
        .attach('file', p);

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(res.body.created_users).toBe(2); // 2 students (parent is seeded, not created here)
      expect(res.body.created_enrollments).toBe(2);
      expect(res.body.linked_parents).toBe(2);
      expect(res.body.created_classes).toBe(2);
      expect(res.body.skipped_duplicates).toBe(0);
      expect(res.body.errors).toEqual([]);
    });

    it('deduplicates on re-import (same email)', async () => {
      const stamp = Date.now();
      const email = `legacy.dedup.${stamp}@example.test`;
      createdStudentEmails.push(email);

      const csv = [
        'first_name,last_name,email,class_name',
        `Dedup,User,${email},Form 9A`,
      ].join('\n');

      // First commit: creates the student
      const p1 = writeCsv('commit-dedup-1.csv', csv);
      const r1 = await request(app)
        .post('/api/legacy-import/commit')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p1);
      expect(r1.status).toBe(200);
      expect(r1.body.created_users).toBe(1);

      // Second commit with the same data: dedup hit
      const p2 = writeCsv('commit-dedup-2.csv', csv);
      const r2 = await request(app)
        .post('/api/legacy-import/commit')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p2);
      expect(r2.status).toBe(200);
      expect(r2.body.created_users).toBe(0);
      expect(r2.body.skipped_duplicates).toBe(1);
    });

    it('rolls back the entire batch when a single row fails (default)', async () => {
      const stamp = Date.now();
      const email = `legacy.rollback.${stamp}@example.test`;
      createdStudentEmails.push(email);

      const csv = [
        'first_name,last_name,email',
        `Good,Row,${email}`,
        ',MissingLast,will.fail@example.test', // missing first_name AND last_name
      ].join('\n');
      const p = writeCsv('commit-rollback.csv', csv);

      const res = await request(app)
        .post('/api/legacy-import/commit')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p);

      expect(res.status).toBe(409);
      expect(res.body.error).toMatch(/rolled back/i);
      expect(res.body.partial_results.errors.length).toBeGreaterThan(0);

      // The good row should NOT have been created (tx rolled back)
      const Database = require('better-sqlite3');
      const db = new Database(process.env.DB_PATH);
      const found = db.prepare('SELECT id FROM users WHERE email = ? AND is_deleted = 0').get(email);
      db.close();
      expect(found).toBeUndefined();
    });

    it('returns 400 when the CSV has no auto-detectable columns', async () => {
      // All column names are unknown to the auto-detector — no mapping
      // can be inferred and the caller did not pass an explicit one.
      const csv = 'foo,bar,baz\n1,2,3';
      const p = writeCsv('commit-no-mapping.csv', csv);
      const res = await request(app)
        .post('/api/legacy-import/commit')
        .set('Authorization', `Bearer ${adminToken}`)
        .attach('file', p);
      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/mapping/i);
      expect(res.body.detected_headers).toEqual(['foo', 'bar', 'baz']);
    });
  });

  // ==================== Error CSV ====================

  describe('POST /api/legacy-import/error-csv', () => {
    it('returns a CSV with row_number + errors', async () => {
      const res = await request(app)
        .post('/api/legacy-import/error-csv')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          errors: [
            { row_number: 2, errors: ['missing first_name', 'invalid email'] },
            { row_number: 5, errors: ['missing last_name'] },
          ],
        });
      expect(res.status).toBe(200);
      expect(res.headers['content-type']).toMatch(/text\/csv/);
      expect(res.headers['content-disposition']).toMatch(/legacy-import-errors\.csv/);
      const body = res.text;
      expect(body).toMatch(/^.{0,3}row_number,errors/); // BOM is OK
      expect(body).toMatch(/2,"missing first_name; invalid email"/);
      expect(body).toMatch(/5,"missing last_name"/);
    });
  });

  // ==================== Mappings ====================

  describe('Legacy mappings CRUD', () => {
    it('lists, gets, and deletes a saved mapping', async () => {
      const list = await request(app)
        .get('/api/legacy-import/mappings')
        .set('Authorization', `Bearer ${adminToken}`);
      expect(list.status).toBe(200);
      expect(Array.isArray(list.body.mappings)).toBe(true);
      if (list.body.mappings.length > 0) {
        const uid = list.body.mappings[0].uid;
        const get = await request(app)
          .get(`/api/legacy-import/mappings/${uid}`)
          .set('Authorization', `Bearer ${adminToken}`);
        expect(get.status).toBe(200);
        expect(get.body.uid).toBe(uid);
        expect(get.body.mapping).toBeDefined();

        const del = await request(app)
          .delete(`/api/legacy-import/mappings/${uid}`)
          .set('Authorization', `Bearer ${adminToken}`);
        expect(del.status).toBe(200);
        expect(del.body.success).toBe(true);

        const get2 = await request(app)
          .get(`/api/legacy-import/mappings/${uid}`)
          .set('Authorization', `Bearer ${adminToken}`);
        expect(get2.status).toBe(404);
      }
    });
  });

  afterAll(() => {
    // Clean up the temp files; the inserted students are soft-stale
    // (sync_status = 'pending') and will be picked up by the next
    // Supabase sync if it's ever configured. For now we leave them
    // in the dev DB — the test data is identifiable by the email
    // pattern `legacy.*@example.test`.
    try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (_) { /* ignore */ }
  });
});
