import nock from 'nock';

import { DM } from '../../../src/bin';
import CalendarResources from '../../../src/plugins/twake/calendarResources';
import { expect } from 'chai';
import OnLdapChange from '../../../src/plugins/ldap/onChange';
import LdapFlat from '../../../src/plugins/ldap/flatGeneric';

describe('Calendar Resources Plugin', function () {
  // Skip all tests if required env vars are not set
  if (
    !process.env.DM_LDAP_DN ||
    !process.env.DM_LDAP_PWD ||
    !process.env.DM_LDAP_BASE
  ) {
    // eslint-disable-next-line no-console
    console.warn(
      'Skipping Calendar Resources tests: DM_LDAP_DN or DM_LDAP_PWD or DM_LDAP_BASE not set'
    );
    // @ts-ignore
    this.skip?.();
    return;
  }

  let resourceBase: string;
  let testResourceDN: string;
  let dm: DM;
  let calendarResources: CalendarResources;
  let ldapFlat: LdapFlat;
  let resourceInstance: any; // The resources instance from ldapFlat
  let scope: nock.Scope;

  before(function () {
    nock.disableNetConnect();
  });

  after(function () {
    nock.cleanAll();
    nock.enableNetConnect();
  });

  beforeEach(async function () {
    this.timeout(5000);

    dm = new DM();
    // Add schema path for ldapFlat
    dm.config.ldap_flat_schema = [
      'test/fixtures/calendar-resources-schema.json',
    ];
    // Force ldap_base from env BEFORE ready (in case another test modified process.env)
    dm.config.ldap_base = process.env.DM_LDAP_BASE;
    await dm.ready;

    // Initialize resource paths from env
    resourceBase = `ou=resources,${process.env.DM_LDAP_BASE}`;
    testResourceDN = `cn=Conference Room A,${resourceBase}`;

    // Ensure ou=resources exists BEFORE creating plugins
    try {
      await dm.ldap.add(resourceBase, {
        objectClass: ['organizationalUnit', 'top'],
        ou: 'resources',
      });
    } catch (err) {
      // Ignore if already exists
    }

    calendarResources = new CalendarResources(dm);
    ldapFlat = new LdapFlat(dm);
    resourceInstance = ldapFlat.instances[0];

    await dm.registerPlugin('onLdapChange', new OnLdapChange(dm));
    await dm.registerPlugin('ldapFlat', ldapFlat);
    await dm.registerPlugin('calendarResources', calendarResources);
  });

  afterEach(async () => {
    // Clean up test data
    try {
      await dm.ldap.delete(testResourceDN);
    } catch (err) {
      // Ignore errors if the entry does not exist
    }
  });

  it('should create resource in Calendar when added to LDAP', async () => {
    // Track API calls
    let calendarApiCalled = false;
    const apiScope = nock(
      process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
    )
      .post('/resources', body => {
        calendarApiCalled = true;
        expect(body).to.have.property('name', 'Conference Room A');
        expect(body).to.have.property('description', 'Large meeting room');
        return true;
      })
      .reply(201);

    const res = await resourceInstance.addEntry('Conference Room A', {
      description: 'Large meeting room',
    });
    expect(res).to.be.true;

    // Wait for async hooks to complete
    await new Promise(resolve => setTimeout(resolve, 50));

    expect(calendarApiCalled).to.be.true;

    apiScope.persist(false);
    nock.cleanAll();
  });

  it('should update resource in Calendar when modified in LDAP', async () => {
    // Mock both create and update API calls
    const createScope = nock(
      process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
    )
      .post('/resources')
      .reply(201);

    // First create the resource
    await resourceInstance.addEntry('Conference Room A', {
      description: 'Large meeting room',
    });

    // Wait for create hook to complete
    await new Promise(resolve => setTimeout(resolve, 50));

    // Track update API call
    let calendarApiCalled = false;
    const updateScope = nock(
      process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
    )
      .patch('/resources/Conference%20Room%20A', body => {
        calendarApiCalled = true;
        expect(body).to.have.property('description', 'Updated description');
        return true;
      })
      .reply(204);

    // Then modify it
    const res = await resourceInstance.modifyEntry(testResourceDN, {
      replace: { description: 'Updated description' },
    });
    expect(res).to.be.true;

    // Wait for async hooks to complete
    await new Promise(resolve => setTimeout(resolve, 50));

    expect(calendarApiCalled).to.be.true;

    createScope.persist(false);
    updateScope.persist(false);
    nock.cleanAll();
  });

  it('should delete resource from Calendar when deleted from LDAP', async () => {
    // Mock create API call
    const createScope = nock(
      process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
    )
      .post('/resources')
      .reply(201);

    // First create the resource
    await resourceInstance.addEntry('Conference Room A', {
      description: 'Large meeting room',
    });

    // Wait for create hook to complete
    await new Promise(resolve => setTimeout(resolve, 50));

    // Track delete API call
    let calendarApiCalled = false;
    const deleteScope = nock(
      process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
    )
      .delete('/resources/Conference%20Room%20A')
      .reply(function () {
        calendarApiCalled = true;
        return [204];
      });

    // Then delete it
    const res = await resourceInstance.deleteEntry(testResourceDN);
    expect(res).to.be.true;

    // Wait for async hooks to complete
    await new Promise(resolve => setTimeout(resolve, 50));

    expect(calendarApiCalled).to.be.true;

    createScope.persist(false);
    deleteScope.persist(false);
    nock.cleanAll();
  });

  describe('deleteUserData', () => {
    it('should call POST /users/{mail}?action=deleteData and return taskId', async () => {
      const deleteDataScope = nock(
        process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
      )
        .post('/users/user@test.org?action=deleteData')
        .reply(201, { taskId: 'calendar-task-123' });

      const result = await calendarResources.deleteUserData('user@test.org');

      expect(result).to.deep.equal({ taskId: 'calendar-task-123' });
      expect(deleteDataScope.isDone()).to.be.true;

      nock.cleanAll();
    });

    it('should return null and log error on non-OK response', async () => {
      const deleteDataScope = nock(
        process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080'
      )
        .post('/users/baduser@test.org?action=deleteData')
        .reply(400, { error: 'Bad request' });

      const result = await calendarResources.deleteUserData('baduser@test.org');

      expect(result).to.be.null;
      expect(deleteDataScope.isDone()).to.be.true;

      nock.cleanAll();
    });
  });

  describe('registered users sync', () => {
    let userDN: string;
    const calendarUrl =
      process.env.DM_CALENDAR_WEBADMIN_URL || 'http://localhost:8080';

    beforeEach(async () => {
      userDN = `uid=caluser,${process.env.DM_LDAP_BASE}`;
      try {
        await dm.ldap.add(userDN, {
          objectClass: ['inetOrgPerson', 'top'],
          uid: 'caluser',
          cn: 'Benoit TELLIER',
          givenName: 'Benoit',
          sn: 'TELLIER',
          mail: 'caluser@test.org',
        });
      } catch (err) {
        // Ignore if already exists
      }
      // Creating the entry fires onLdapChange with the mail attribute present,
      // which the plugin treats as an addition and skips — so no stray
      // /registeredUsers call races with the interceptors set up below.
    });

    afterEach(async () => {
      try {
        await dm.ldap.delete(userDN);
      } catch (err) {
        // Ignore if it does not exist
      }
      nock.cleanAll();
    });

    it('lists registered users and PATCHes the one matching the email', async () => {
      let patchBody: Record<string, unknown> | null = null;
      let patchUri = '';
      const scope = nock(calendarUrl)
        .get('/registeredUsers')
        .reply(200, [
          {
            id: '5f50a663',
            email: 'caluser@test.org',
            firstname: 'Old',
            lastname: 'Name',
          },
          { id: 'other', email: 'someoneelse@test.org' },
        ])
        .patch('/registeredUsers', body => {
          patchBody = body as Record<string, unknown>;
          return true;
        })
        .query(true)
        .reply(function (uri) {
          patchUri = uri;
          return [204];
        });

      await calendarResources.syncRegisteredUser('test', userDN);

      expect(scope.isDone()).to.be.true;
      expect(patchUri).to.contain('id=5f50a663');
      expect(patchBody).to.deep.equal({
        email: 'caluser@test.org',
        firstname: 'Benoit',
        lastname: 'TELLIER',
      });
    });

    it('looks up by the OLD email on mail change and PATCHes the new email', async () => {
      let patchBody: Record<string, unknown> | null = null;
      let patchUri = '';
      const scope = nock(calendarUrl)
        .get('/registeredUsers')
        .reply(200, [
          {
            id: 'abc123',
            email: 'old@test.org',
            firstname: 'Benoit',
            lastname: 'TELLIER',
          },
        ])
        .patch('/registeredUsers', body => {
          patchBody = body as Record<string, unknown>;
          return true;
        })
        .query(true)
        .reply(function (uri) {
          patchUri = uri;
          return [204];
        });

      // LDAP now holds caluser@test.org; Calendar still has old@test.org
      await calendarResources.hooks.onLdapChange!(userDN, {
        mail: ['old@test.org', 'caluser@test.org'],
      });

      expect(scope.isDone()).to.be.true;
      expect(patchUri).to.contain('id=abc123');
      expect(patchBody).to.deep.equal({
        email: 'caluser@test.org',
        firstname: 'Benoit',
        lastname: 'TELLIER',
      });
    });

    it('does not PATCH when the user is not registered in Calendar', async () => {
      const scope = nock(calendarUrl)
        .get('/registeredUsers')
        .reply(200, [{ id: 'other', email: 'someoneelse@test.org' }]);

      await calendarResources.syncRegisteredUser('test', userDN);

      // GET happened, no PATCH was issued (would throw on unmocked request)
      expect(scope.isDone()).to.be.true;
    });

    it('skips sync when the mail is added (no previous mail)', async () => {
      // No nock scope: any HTTP call would throw (netConnect disabled)
      await calendarResources.hooks.onLdapChange!(userDN, {
        mail: [null, 'caluser@test.org'],
      });
      // Reaching here without a thrown unmocked-request error proves no call
      expect(nock.pendingMocks()).to.have.length(0);
    });

    it('syncs names when a configured name attribute changes', async () => {
      let patchBody: Record<string, unknown> | null = null;
      let patchUri = '';
      const scope = nock(calendarUrl)
        .get('/registeredUsers')
        .reply(200, [
          {
            id: 'nm1',
            email: 'caluser@test.org',
            firstname: 'Old',
            lastname: 'Name',
          },
        ])
        .patch('/registeredUsers', body => {
          patchBody = body as Record<string, unknown>;
          return true;
        })
        .query(true)
        .reply(function (uri) {
          patchUri = uri;
          return [204];
        });

      // sn is the default configured lastname attribute
      await calendarResources.hooks.onLdapChange!(userDN, {
        sn: ['Name', 'TELLIER'],
      });

      expect(scope.isDone()).to.be.true;
      expect(patchUri).to.contain('id=nm1');
      expect(patchBody).to.deep.equal({
        email: 'caluser@test.org',
        firstname: 'Benoit',
        lastname: 'TELLIER',
      });
    });

    it('ignores changes to unrelated attributes', async () => {
      // No nock scope: any HTTP call would throw (netConnect disabled)
      await calendarResources.hooks.onLdapChange!(userDN, {
        description: ['before', 'after'],
      });
      expect(nock.pendingMocks()).to.have.length(0);
    });
  });
});
