/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: MPL-2.0
 */

import { run } from '@ember/runloop';
import { module, test } from 'qunit';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import { setupTest } from 'ember-qunit';
import { settled } from '@ember/test-helpers';

module('Unit | Adapter | Node', function (hooks) {
  setupTest(hooks);

  hooks.beforeEach(function () {
    this.store = this.owner.lookup('service:store');
    this.subject = () => this.store.adapterFor('node');

    window.localStorage.clear();

    this.server = startMirage();

    this.server.create('region', { id: 'region-1' });
    this.server.create('region', { id: 'region-2' });

    this.server.create('node-pool');
    this.server.create('node', { id: 'node-1' });
    this.server.create('node', { id: 'node-2' });
    this.server.create('job', { id: 'job-1', createAllocations: false });

    this.server.create('allocation', { id: 'node-1-1', nodeId: 'node-1' });
    this.server.create('allocation', { id: 'node-1-2', nodeId: 'node-1' });
    this.server.create('allocation', { id: 'node-2-1', nodeId: 'node-2' });
    this.server.create('allocation', { id: 'node-2-2', nodeId: 'node-2' });
  });

  hooks.afterEach(function () {
    this.server.shutdown();
  });

  test('findHasMany removes old related models from the store', async function (assert) {
    // Fetch the model and related allocations
    let node = await run(() => this.store.findRecord('node', 'node-1'));
    let allocations = await run(() => findHasMany(node, 'allocations'));
    assert.equal(
      allocations.get('length'),
      this.server.db.allocations.where({ nodeId: node.get('id') }).length,
      'Allocations returned from the findHasMany matches the db state'
    );

    await settled();
    server.db.allocations.remove('node-1-1');

    allocations = await run(() => findHasMany(node, 'allocations'));
    const dbAllocations = this.server.db.allocations.where({
      nodeId: node.get('id'),
    });
    assert.equal(
      allocations.get('length'),
      dbAllocations.length,
      'Allocations returned from the findHasMany matches the db state'
    );
    assert.equal(
      this.store.peekAll('allocation').get('length'),
      dbAllocations.length,
      'Server-side deleted allocation was removed from the store'
    );
  });

  test('findHasMany does not remove old unrelated models from the store', async function (assert) {
    // Fetch the first node and related allocations
    const node = await run(() => this.store.findRecord('node', 'node-1'));
    await run(() => findHasMany(node, 'allocations'));

    // Also fetch the second node and related allocations;
    const node2 = await run(() => this.store.findRecord('node', 'node-2'));
    await run(() => findHasMany(node2, 'allocations'));

    await settled();
    assert.deepEqual(
      this.store.peekAll('allocation').mapBy('id').sort(),
      ['node-1-1', 'node-1-2', 'node-2-1', 'node-2-2'],
      'All allocations for the first and second node are in the store'
    );

    server.db.allocations.remove('node-1-1');

    // Reload the related allocations now that one was removed server-side
    await run(() => findHasMany(node, 'allocations'));
    assert.deepEqual(
      this.store.peekAll('allocation').mapBy('id').sort(),
      ['node-1-2', 'node-2-1', 'node-2-2'],
      'The deleted allocation is removed from the store and the allocations associated with the other node are untouched'
    );
  });

  const testCases = [
    {
      variation: '',
      id: 'node-1',
      region: null,
      eligibility: 'POST /v1/node/node-1/eligibility',
      drain: 'POST /v1/node/node-1/drain',
    },
    {
      variation: 'with non-default region',
      id: 'node-1',
      region: 'region-2',
      eligibility: 'POST /v1/node/node-1/eligibility?region=region-2',
      drain: 'POST /v1/node/node-1/drain?region=region-2',
    },
  ];

  testCases.forEach((testCase) => {
    test(`setEligible makes the correct POST request to /:node_id/eligibility ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));
      await this.subject().setEligible(node);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.eligibility);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        Eligibility: 'eligible',
      });
    });

    test(`setIneligible makes the correct POST request to /:node_id/eligibility ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));
      await this.subject().setIneligible(node);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.eligibility);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        Eligibility: 'ineligible',
      });
    });

    test(`drain makes the correct POST request to /:node_id/drain with appropriate defaults ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));
      await this.subject().drain(node);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.drain);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        DrainSpec: {
          Deadline: 0,
          IgnoreSystemJobs: true,
        },
      });
    });

    test(`drain makes the correct POST request to /:node_id/drain with the provided drain spec ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));

      const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
      await this.subject().drain(node, spec);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.drain);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        DrainSpec: {
          Deadline: spec.Deadline,
          IgnoreSystemJobs: spec.IgnoreSystemJobs,
        },
      });
    });

    test(`forceDrain makes the correct POST request to /:node_id/drain with appropriate defaults ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));

      await this.subject().forceDrain(node);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.drain);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        DrainSpec: {
          Deadline: -1,
          IgnoreSystemJobs: true,
        },
      });
    });

    test(`forceDrain makes the correct POST request to /:node_id/drain with the provided drain spec ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));

      const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
      await this.subject().forceDrain(node, spec);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.drain);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        DrainSpec: {
          Deadline: -1,
          IgnoreSystemJobs: spec.IgnoreSystemJobs,
        },
      });
    });

    test(`cancelDrain makes the correct POST request to /:node_id/drain ${testCase.variation}`, async function (assert) {
      const { pretender } = this.server;
      if (testCase.region)
        window.localStorage.nomadActiveRegion = testCase.region;

      const node = await run(() => this.store.findRecord('node', testCase.id));

      await this.subject().cancelDrain(node);

      const request = pretender.handledRequests.lastObject;
      assert.equal(`${request.method} ${request.url}`, testCase.drain);
      assert.deepEqual(JSON.parse(request.requestBody), {
        NodeID: node.id,
        DrainSpec: null,
      });
    });
  });
});

// Using fetchLink on a model's hasMany relationship exercises the adapter's
// findHasMany method as well normalizing the response and pushing it to the store
function findHasMany(model, relationshipName) {
  const relationship = model.relationshipFor(relationshipName);
  return model.hasMany(relationship.key).reload();
}
