import Database from 'better-sqlite3';
type SqliteDB = InstanceType<typeof Database>;

/**
 * Real SQLite foreign-key constraint tests.
 *
 * These tests use the `better-sqlite3` driver directly (in-memory) to verify
 * that ON DELETE RESTRICT is enforced at the database level, not just in
 * application-layer guards.
 */
describe('attendance_sessions FK RESTRICT — real SQLite', () => {
  let db: SqliteDB;

  function createSchema(): void {
    db.exec('PRAGMA foreign_keys = ON');
    db.exec(`
      CREATE TABLE IF NOT EXISTS classes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        is_archived INTEGER DEFAULT 0
      )
    `);
    db.exec(`
      CREATE TABLE IF NOT EXISTS class_schedule (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        class_id INTEGER,
        week_day INTEGER NOT NULL
      )
    `);
    db.exec(`
      CREATE TABLE IF NOT EXISTS attendance_sessions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        schedule_id INTEGER NOT NULL,
        class_id INTEGER NOT NULL,
        lesson_date DATE NOT NULL,
        status TEXT DEFAULT 'in_progress',
        FOREIGN KEY (schedule_id) REFERENCES class_schedule(id) ON DELETE RESTRICT,
        FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE RESTRICT
      )
    `);
  }

  beforeEach(() => {
    db = new Database(':memory:');
    createSchema();
  });

  afterEach(() => {
    db.close();
  });

  it('blocks class deletion when attendance sessions reference it', () => {
    db.exec("INSERT INTO classes (id, name) VALUES (1, 'Test Class')");
    db.exec("INSERT INTO class_schedule (id, class_id, week_day) VALUES (1, 1, 1)");
    db.exec(
      "INSERT INTO attendance_sessions (id, schedule_id, class_id, lesson_date) VALUES (1, 1, 1, '2026-01-01')",
    );

    expect(() => {
      db.exec('DELETE FROM classes WHERE id = 1');
    }).toThrow();
  });

  it('allows class deletion when no attendance sessions reference it', () => {
    db.exec("INSERT INTO classes (id, name) VALUES (1, 'Test Class')");

    expect(() => {
      db.exec('DELETE FROM classes WHERE id = 1');
    }).not.toThrow();

    const remaining = db.prepare('SELECT COUNT(*) as cnt FROM classes').get() as {
      cnt: number;
    };
    expect(remaining.cnt).toBe(0);
  });

  it('blocks schedule deletion when attendance sessions reference it', () => {
    db.exec("INSERT INTO classes (id, name) VALUES (1, 'Test Class')");
    db.exec("INSERT INTO class_schedule (id, class_id, week_day) VALUES (1, 1, 1)");
    db.exec(
      "INSERT INTO attendance_sessions (id, schedule_id, class_id, lesson_date) VALUES (1, 1, 1, '2026-01-01')",
    );

    expect(() => {
      db.exec('DELETE FROM class_schedule WHERE id = 1');
    }).toThrow();
  });

  it('PRAGMA foreign_key_list confirms both FKs are present', () => {
    // Use raw SQL PRAGMA to avoid better-sqlite3 pragma API quirks
    const rows = db.prepare("PRAGMA foreign_key_list('attendance_sessions')").all() as Array<{
      id: number;
      seq: number;
      table: string;
      from: string;
      to: string;
      on_update: string;
      on_delete: string;
      match: string;
    }>;

    expect(rows.length).toBe(2);

    const scheduleFk = rows.find((fk) => fk.from === 'schedule_id');
    expect(scheduleFk).toBeDefined();
    expect(scheduleFk!.table).toBe('class_schedule');
    expect(scheduleFk!.on_delete).toBe('RESTRICT');

    const classFk = rows.find((fk) => fk.from === 'class_id');
    expect(classFk).toBeDefined();
    expect(classFk!.table).toBe('classes');
    expect(classFk!.on_delete).toBe('RESTRICT');
  });

  it('FK pragma respects ON DELETE RESTRICT for class_id — data survives failed delete', () => {
    db.exec("INSERT INTO classes (id, name) VALUES (1, 'Test Class')");
    db.exec("INSERT INTO class_schedule (id, class_id, week_day) VALUES (1, 1, 1)");
    db.exec(
      "INSERT INTO attendance_sessions (id, schedule_id, class_id, lesson_date) VALUES (1, 1, 1, '2026-01-01')",
    );

    // Verify the session exists
    const session = db
      .prepare('SELECT * FROM attendance_sessions WHERE class_id = 1')
      .get() as Record<string, unknown>;
    expect(session).toBeDefined();

    // Delete should fail
    expect(() => db.exec('DELETE FROM classes WHERE id = 1')).toThrow();

    // Session should still exist after failed delete
    const after = db
      .prepare('SELECT COUNT(*) as cnt FROM attendance_sessions WHERE class_id = 1')
      .get() as { cnt: number };
    expect(after.cnt).toBe(1);
  });
});

/**
 * Integration test: simulate the protectAttendanceHistory SQLite migration.
 *
 * Creates tables WITHOUT foreign keys (pre-migration state), inserts parent
 * session and child attendance_record, runs the table-rebuild migration
 * (PRAGMA foreign_keys=OFF, rebuild both tables, PRAGMA foreign_keys=ON,
 * foreign_key_check), then verifies:
 *  - attendance_record.attendance_session_id is preserved
 *  - RESTRICT still blocks class/schedule deletion
 */
describe('protectAttendanceHistory SQLite migration — integration', () => {
  let db: SqliteDB;

  function createPreMigrationSchema(): void {
    // Schema WITHOUT foreign keys on attendance_sessions (pre-migration)
    db.exec('PRAGMA foreign_keys = ON');
    db.exec(`
      CREATE TABLE IF NOT EXISTS classes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        is_archived INTEGER DEFAULT 0
      )
    `);
    db.exec(`
      CREATE TABLE IF NOT EXISTS class_schedule (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        class_id INTEGER,
        week_day INTEGER NOT NULL
      )
    `);
    // attendance_sessions WITHOUT foreign keys
    db.exec(`
      CREATE TABLE IF NOT EXISTS attendance_sessions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        schedule_id INTEGER NOT NULL,
        class_id INTEGER NOT NULL,
        lesson_date DATE NOT NULL,
        status TEXT DEFAULT 'in_progress',
        started_by INTEGER,
        started_at DATETIME,
        completed_by INTEGER,
        completed_at DATETIME,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
      )
    `);
    // Legacy columns came first; course-attendance columns were appended later.
    db.exec(`
      CREATE TABLE IF NOT EXISTS attendance_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER NOT NULL,
        class_id INTEGER,
        attendance_date DATE NOT NULL,
        session VARCHAR(20) NOT NULL,
        status VARCHAR(20) NOT NULL,
        remark VARCHAR(200),
        source VARCHAR(20) DEFAULT 'manual',
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        schedule_id INTEGER,
        attendance_session_id INTEGER
      )
    `);
  }

  function runMigration(): void {
    // Step 1: PRAGMA foreign_keys = OFF outside transaction
    db.exec('PRAGMA foreign_keys = OFF');
    try {
      db.exec('BEGIN');
      try {
        // Rebuild attendance_sessions with FKs
        db.exec(`
          CREATE TABLE attendance_sessions_new (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            schedule_id INTEGER NOT NULL,
            class_id INTEGER NOT NULL,
            lesson_date DATE NOT NULL,
            status VARCHAR(20) NOT NULL DEFAULT 'in_progress',
            started_by INTEGER,
            started_at DATETIME,
            completed_by INTEGER,
            completed_at DATETIME,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (schedule_id) REFERENCES class_schedule(id) ON DELETE RESTRICT,
            FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE RESTRICT
          )
        `);
        db.exec(
          'INSERT INTO attendance_sessions_new SELECT * FROM attendance_sessions',
        );
        db.exec('DROP TABLE attendance_sessions');
        db.exec(
          'ALTER TABLE attendance_sessions_new RENAME TO attendance_sessions',
        );
        db.exec(
          'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_schedule_date ON attendance_sessions(schedule_id, lesson_date)',
        );

        // Rebuild attendance_records with FK on attendance_session_id
        const recordsFk = db
          .prepare("PRAGMA foreign_key_list('attendance_records')")
          .all() as Array<{ from: string }>;
        const hasSessionFk = recordsFk.some((r) => r.from === 'attendance_session_id');
        if (!hasSessionFk) {
          db.exec(`
            CREATE TABLE attendance_records_new (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              student_id INTEGER NOT NULL,
              class_id INTEGER,
              schedule_id INTEGER,
              attendance_session_id INTEGER,
              attendance_date DATE NOT NULL,
              session VARCHAR(20) NOT NULL,
              status VARCHAR(20) NOT NULL,
              remark VARCHAR(200),
              source VARCHAR(20) DEFAULT 'manual',
              created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
              updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
              FOREIGN KEY (attendance_session_id) REFERENCES attendance_sessions(id) ON DELETE SET NULL
            )
          `);
          db.exec(`
            INSERT INTO attendance_records_new (
              id, student_id, class_id, schedule_id, attendance_session_id,
              attendance_date, session, status, remark, source, created_at, updated_at
            )
            SELECT
              id, student_id, class_id, schedule_id, attendance_session_id,
              attendance_date, session, status, remark, source, created_at, updated_at
            FROM attendance_records
          `);
          db.exec('DROP TABLE attendance_records');
          db.exec(
            'ALTER TABLE attendance_records_new RENAME TO attendance_records',
          );
          db.exec(
            'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_student ON attendance_records(attendance_session_id, student_id)',
          );
        }

        db.exec('COMMIT');
      } catch (err) {
        db.exec('ROLLBACK');
        throw err;
      }
    } finally {
      db.exec('PRAGMA foreign_keys = ON');
    }

    // Run foreign_key_check — should be clean
    const checkRows = db.prepare('PRAGMA foreign_key_check').all();
    if (checkRows.length > 0) {
      throw new Error(
        `外键一致性检查失败: ${checkRows.length} 行违反外键约束`,
      );
    }
  }

  beforeEach(() => {
    db = new Database(':memory:');
    createPreMigrationSchema();
  });

  afterEach(() => {
    db.close();
  });

  it('preserves attendance_record.session_id after migration', () => {
    db.exec("INSERT INTO classes (id, name) VALUES (1, 'Test Class')");
    db.exec("INSERT INTO class_schedule (id, class_id, week_day) VALUES (1, 1, 1)");
    db.exec(
      "INSERT INTO attendance_sessions (id, schedule_id, class_id, lesson_date) VALUES (1, 1, 1, '2026-01-01')",
    );
    db.exec(
      "INSERT INTO attendance_records (id, student_id, class_id, attendance_session_id, attendance_date, session, status) VALUES (1, 1, 1, 1, '2026-01-01', 'morning', 'present')",
    );

    // Verify pre-migration state
    const preSessionFk = db
      .prepare("PRAGMA foreign_key_list('attendance_sessions')")
      .all();
    expect(preSessionFk.length).toBe(0);

    const preRecordsFk = db
      .prepare("PRAGMA foreign_key_list('attendance_records')")
      .all();
    expect(preRecordsFk.length).toBe(0);

    // Run migration
    runMigration();

    // Verify attendance_record still has correct attendance_session_id
    const record = db
      .prepare('SELECT * FROM attendance_records WHERE id = 1')
      .get() as Record<string, unknown>;
    expect(record).toBeDefined();
    expect(record.attendance_session_id).toBe(1);
    expect(record.attendance_date).toBe('2026-01-01');
    expect(record.session).toBe('morning');
    expect(record.status).toBe('present');

    // Verify FKs now exist on both tables
    const postSessionFk = db
      .prepare("PRAGMA foreign_key_list('attendance_sessions')")
      .all();
    expect(postSessionFk.length).toBe(2);

    const postRecordsFk = db
      .prepare("PRAGMA foreign_key_list('attendance_records')")
      .all() as Array<{ from: string; table: string; on_delete: string }>;
    const sessionFk = postRecordsFk.find((r) => r.from === 'attendance_session_id');
    expect(sessionFk).toBeDefined();
    expect(sessionFk!.table).toBe('attendance_sessions');
    expect(sessionFk!.on_delete).toBe('SET NULL');

    // RESTRICT still blocks class/schedule deletion
    expect(() => {
      db.exec('DELETE FROM classes WHERE id = 1');
    }).toThrow();
    expect(() => {
      db.exec('DELETE FROM class_schedule WHERE id = 1');
    }).toThrow();

    // Verify data survived the failed deletes
    const sessionAfter = db
      .prepare('SELECT COUNT(*) as cnt FROM attendance_sessions WHERE id = 1')
      .get() as { cnt: number };
    expect(sessionAfter.cnt).toBe(1);

    const recordAfter = db
      .prepare('SELECT COUNT(*) as cnt FROM attendance_records WHERE id = 1')
      .get() as { cnt: number };
    expect(recordAfter.cnt).toBe(1);

    const classAfter = db
      .prepare('SELECT COUNT(*) as cnt FROM classes WHERE id = 1')
      .get() as { cnt: number };
    expect(classAfter.cnt).toBe(1);
  });
});
