import nock from 'nock';
import { expect } from 'chai';

import { DM } from '../../../src/bin';
import CozyProvision from '../../../src/plugins/twake/cozyProvision';
import type { ScimUser } from '../../../src/plugins/scim/types';

interface PublishCall {
  exchange: string;
  routingKey: string;
  message: Record<string, unknown>;
}

/**
 * Stand-in for the shared `rabbitmq` plugin, injected into the DM's
 * loaded-plugin registry so cozyProvision publishes through it without a
 * real broker.
 */
class StubRabbitMq {
  name = 'rabbitmq';
  calls: PublishCall[] = [];
  isAvailable(): boolean {
    return true;
  }
  async publish(
    exchange: string,
    routingKey: string,
    message: Record<string, unknown>
  ): Promise<void> {
    this.calls.push({ exchange, routingKey, message });
  }
}

const COZY_URL = 'http://cozyt:6060';

describe('CozyProvision plugin', () => {
  let dm: DM;
  let plugin: CozyProvision;
  let rabbit: StubRabbitMq;

  before(() => {
    nock.disableNetConnect();
  });

  after(() => {
    nock.cleanAll();
    nock.enableNetConnect();
  });

  beforeEach(async () => {
    dm = new DM();
    dm.config.cozy_admin_url = COZY_URL;
    dm.config.cozy_admin_user = 'admin';
    dm.config.cozy_admin_passphrase = 'admin';
    dm.config.cozy_org_id = 'twp-test';
    dm.config.cozy_org_domain = 'twake.local';
    dm.config.cozy_default_locale = 'fr';
    // Non-empty url just so the lazy-init path is exercised; the stub
    // overrides actual broker access.
    dm.config.rabbitmq_url = 'amqp://guest:guest@rabbitmq:5672/';
    await dm.ready;
    rabbit = new StubRabbitMq();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    dm.loadedPlugins['rabbitmq'] = rabbit as any;
    plugin = new CozyProvision(dm);
  });

  afterEach(() => {
    nock.cleanAll();
  });

  describe('scimusercreatedone', () => {
    it('POSTs /instances on cozy admin and publishes user.created', async () => {
      const scope = nock(COZY_URL)
        .post('/instances')
        .query({
          Domain: 'alice.twake.local',
          Locale: 'fr',
          Email: 'alice@twake.local',
          PublicName: 'alice',
          OrgID: 'twp-test',
          OrgDomain: 'twake.local',
          ContextName: 'default',
          Apps: 'home,drive,settings,notes,dataproxy',
          OIDCID: 'alice',
        })
        .matchHeader('authorization', /^Basic /)
        .reply(201, { ok: true })
        .patch('/instances/alice.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        id: 'alice',
        userName: 'alice',
        emails: [{ value: 'alice@twake.local', primary: true }],
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(scope.isDone(), 'cozy admin POST + PATCH').to.be.true;
      expect(rabbit.calls).to.have.length(1);
      const call = rabbit.calls[0];
      expect(call.exchange).to.equal('auth');
      expect(call.routingKey).to.equal('user.created');
      expect(call.message).to.deep.include({
        twakeId: 'alice',
        domain: 'twake.local',
        internalEmail: 'alice@twake.local',
        organizationDomain: 'twake.local',
        workplaceFqdn: 'alice.twake.local',
      });
    });

    it('publishes with a configured routing key', async () => {
      dm.config.cozy_user_created_routing_key = 'custom.user.created';
      const p = new CozyProvision(dm);
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(true)
        .reply(201, { ok: true })
        .patch('/instances/ivy.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'ivy',
      };

      const hook = p.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(scope.isDone()).to.be.true;
      expect(rabbit.calls).to.have.length(1);
      expect(rabbit.calls[0].exchange).to.equal('auth');
      expect(rabbit.calls[0].routingKey).to.equal('custom.user.created');
    });

    it('respects cozy_apps override', async () => {
      dm.config.cozy_apps = 'home,drive';
      const p = new CozyProvision(dm);
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(q => q.Apps === 'home,drive')
        .reply(201, { ok: true })
        .patch('/instances/ivy.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'ivy',
      };
      const hook = p.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);
      expect(scope.isDone()).to.be.true;
    });

    it('omits Apps query param when cozy_apps is empty', async () => {
      dm.config.cozy_apps = '';
      const p = new CozyProvision(dm);
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(q => !('Apps' in q))
        .reply(201, { ok: true })
        .patch('/instances/jack.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'jack',
      };
      const hook = p.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);
      expect(scope.isDone()).to.be.true;
    });

    it('uses SCIM displayName / name as PublicName when provided', async () => {
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(
          q => q.PublicName === 'Khaled Ferjani' && q.Phone === '+33600000000'
        )
        .reply(201, { ok: true })
        .patch('/instances/khaled.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'khaled',
        displayName: 'Khaled Ferjani',
        phoneNumbers: [{ value: '+33600000000', primary: true }],
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);
      expect(scope.isDone()).to.be.true;
    });

    it('forwards primary phoneNumber as mobile when provided', async () => {
      nock(COZY_URL)
        .post('/instances')
        .query(true)
        .reply(201, { ok: true })
        .patch('/instances/grace.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'grace',
        phoneNumbers: [
          { value: '+33600000000', primary: true },
          { value: '+33700000000' },
        ],
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(rabbit.calls).to.have.length(1);
      expect(rabbit.calls[0].message).to.deep.include({
        twakeId: 'grace',
        mobile: '+33600000000',
      });
    });

    it('treats 409 as success and still publishes', async () => {
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(true)
        .reply(409, { error: 'already exists' });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'bob',
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(scope.isDone()).to.be.true;
      expect(rabbit.calls).to.have.length(1);
      expect(rabbit.calls[0].routingKey).to.equal('user.created');
    });

    it('does not fail the hook when the onboarding PATCH errors', async () => {
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(true)
        .reply(201, { ok: true })
        .patch('/instances/heidi.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(500, { error: 'boom' });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'heidi',
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(scope.isDone()).to.be.true;
      // Publish still happens — PATCH failure must not abort the flow.
      expect(rabbit.calls).to.have.length(1);
      expect(rabbit.calls[0].routingKey).to.equal('user.created');
    });

    it('skips publish when cozy admin returns a hard error', async () => {
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(true)
        .reply(500, { error: 'boom' });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'carol',
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(scope.isDone()).to.be.true;
      expect(rabbit.calls).to.have.length(0);
    });

    it('uses user.locale when present', async () => {
      const scope = nock(COZY_URL)
        .post('/instances')
        .query(q => q.Locale === 'en')
        .reply(201, { ok: true })
        .patch('/instances/dave.twake.local')
        .query({ OnboardingFinished: 'true' })
        .reply(200, { ok: true });

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'dave',
        locale: 'en',
      };

      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      expect(scope.isDone()).to.be.true;
    });

    it('skips when user has no userName/id', async () => {
      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
      };
      const hook = plugin.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);
      // No HTTP call expected, no publish either
      expect(rabbit.calls).to.have.length(0);
    });
  });

  describe('scimuserdeletedone', () => {
    it('DELETEs the cozy instance and publishes domain.user.deleted', async () => {
      const scope = nock(COZY_URL)
        .delete('/instances/eve.twake.local')
        .reply(204);

      const hook = plugin.hooks?.scimuserdeletedone as (
        id: string
      ) => Promise<void>;
      await hook('eve');

      expect(scope.isDone()).to.equal(true);
      expect(rabbit.calls).to.have.length(1);
      const call = rabbit.calls[0];
      expect(call.exchange).to.equal('b2b');
      expect(call.routingKey).to.equal('domain.user.deleted');
      expect(call.message).to.deep.equal({
        workplaceFqdn: 'eve.twake.local',
        domain: 'twake.local',
      });
    });

    it('treats 404 from cozy admin as success and still publishes', async () => {
      const scope = nock(COZY_URL)
        .delete('/instances/ghost.twake.local')
        .reply(404);

      const hook = plugin.hooks?.scimuserdeletedone as (
        id: string
      ) => Promise<void>;
      await hook('ghost');

      expect(scope.isDone()).to.equal(true);
      expect(rabbit.calls).to.have.length(1);
      expect(rabbit.calls[0].routingKey).to.equal('domain.user.deleted');
    });

    it('does not publish when the destroy errors', async () => {
      const scope = nock(COZY_URL)
        .delete('/instances/broken.twake.local')
        .reply(500, { error: 'server error' });

      const hook = plugin.hooks?.scimuserdeletedone as (
        id: string
      ) => Promise<void>;
      await hook('broken');

      expect(scope.isDone()).to.equal(true);
      expect(rabbit.calls).to.have.length(0);
    });
  });

  describe('configuration safety', () => {
    it('does not call cozy admin when cozy_admin_url is empty', async () => {
      const dm2 = new DM();
      dm2.config.cozy_admin_url = '';
      dm2.config.cozy_org_domain = 'twake.local';
      dm2.config.rabbitmq_url = 'amqp://x';
      await dm2.ready;
      const rabbit2 = new StubRabbitMq();
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      dm2.loadedPlugins['rabbitmq'] = rabbit2 as any;
      const p = new CozyProvision(dm2);

      const user: ScimUser = {
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
        userName: 'frank',
      };
      const hook = p.hooks?.scimusercreatedone as (
        u: ScimUser
      ) => Promise<void>;
      await hook(user);

      // No nock expectation set, no HTTP call attempted; no publish either.
      expect(rabbit2.calls).to.have.length(0);
    });
  });
});
