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

/* eslint-disable ember/no-test-module-for */
/* eslint-disable qunit/require-expect */
import { currentURL, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import moment from 'moment';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import moduleForJob, {
  moduleForJobWithClientStatus,
} from 'nomad-ui/tests/helpers/module-for-job';
import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
import percySnapshot from '@percy/ember';

moduleForJob('Acceptance | job detail (batch)', 'allocations', () =>
  server.create('job', {
    type: 'batch',
    shallow: true,
    noActiveDeployment: true,
    createAllocations: true,
    allocStatusDistribution: {
      running: 1,
    },
  })
);

moduleForJob('Acceptance | job detail (system)', 'allocations', () =>
  server.create('job', {
    type: 'system',
    shallow: true,
    noActiveDeployment: true,
    createAllocations: true,
    allocStatusDistribution: {
      running: 1,
    },
  })
);

moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () =>
  server.create('job', {
    type: 'sysbatch',
    shallow: true,
    noActiveDeployment: true,
    createAllocations: true,
    allocStatusDistribution: {
      running: 1,
      failed: 1,
    },
  })
);

moduleForJobWithClientStatus(
  'Acceptance | job detail with client status (sysbatch)',
  () =>
    server.create('job', {
      status: 'running',
      datacenters: ['dc1'],
      type: 'sysbatch',
      createAllocations: false,
      noActiveDeployment: true,
    })
);

moduleForJobWithClientStatus(
  'Acceptance | job detail with client status (sysbatch with namespace)',
  () => {
    const namespace = server.create('namespace', { id: 'test' });
    return server.create('job', {
      status: 'running',
      datacenters: ['dc1'],
      type: 'sysbatch',
      namespaceId: namespace.name,
      createAllocations: false,
      noActiveDeployment: true,
    });
  }
);

moduleForJobWithClientStatus(
  'Acceptance | job detail with client status (sysbatch with namespace and wildcard dc)',
  () => {
    const namespace = server.create('namespace', { id: 'test' });
    return server.create('job', {
      status: 'running',
      datacenters: ['*'],
      type: 'sysbatch',
      namespaceId: namespace.name,
      createAllocations: false,
      noActiveDeployment: true,
    });
  }
);

moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => {
  const parent = server.create('job', 'periodicSysbatch', {
    childrenCount: 1,
    shallow: true,
    datacenters: ['dc1'],
    createAllocations: true,
    allocStatusDistribution: {
      running: 1,
    },
    noActiveDeployment: true,
  });
  return server.db.jobs.where({ parentId: parent.id })[0];
});

moduleForJobWithClientStatus(
  'Acceptance | job detail with client status (sysbatch child)',
  () => {
    const parent = server.create('job', 'periodicSysbatch', {
      childrenCount: 1,
      shallow: true,
      datacenters: ['dc1'],
      noActiveDeployment: true,
    });
    return server.db.jobs.where({ parentId: parent.id })[0];
  }
);

moduleForJobWithClientStatus(
  'Acceptance | job detail with client status (sysbatch child with namespace)',
  () => {
    const namespace = server.create('namespace', { id: 'test' });
    const parent = server.create('job', 'periodicSysbatch', {
      childrenCount: 1,
      shallow: true,
      namespaceId: namespace.name,
      datacenters: ['dc1'],
      noActiveDeployment: true,
    });
    return server.db.jobs.where({ parentId: parent.id })[0];
  }
);

moduleForJobWithClientStatus(
  'Acceptance | job detail with client status (sysbatch child with namespace and wildcard dc)',
  () => {
    const namespace = server.create('namespace', { id: 'test' });
    const parent = server.create('job', 'periodicSysbatch', {
      childrenCount: 1,
      shallow: true,
      namespaceId: namespace.name,
      datacenters: ['*'],
      noActiveDeployment: true,
    });
    return server.db.jobs.where({ parentId: parent.id })[0];
  }
);

moduleForJob(
  'Acceptance | job detail (periodic)',
  'children',
  () => server.create('job', 'periodic', { shallow: true }),
  {
    'the default sort is submitTime descending': async function (job, assert) {
      const mostRecentLaunch = server.db.jobs
        .where({ parentId: job.id })
        .sortBy('submitTime')
        .reverse()[0];

      assert.ok(JobDetail.jobsHeader.hasSubmitTime);
      assert.equal(
        JobDetail.jobs[0].submitTime,
        moment(mostRecentLaunch.submitTime / 1000000).format(
          'MMM DD HH:mm:ss ZZ'
        )
      );
    },
    "don't display redundant information in children table": async function (
      job,
      assert
    ) {
      assert.notOk(JobDetail.jobsHeader.hasNodePool);
      assert.notOk(JobDetail.jobsHeader.hasPriority);
      assert.notOk(JobDetail.jobsHeader.hasType);
    },
  }
);

moduleForJob(
  'Acceptance | job detail (periodic in namespace)',
  'children',
  () => {
    const namespace = server.create('namespace', { id: 'test' });
    const parent = server.create('job', 'periodic', {
      shallow: true,
      namespaceId: namespace.name,
    });
    return parent;
  },
  {
    "don't display namespace in children table": async function (job, assert) {
      assert.notOk(JobDetail.jobsHeader.hasNamespace);
    },
  }
);

moduleForJob(
  'Acceptance | job detail (parameterized)',
  'children',
  () =>
    server.create('job', 'parameterized', {
      shallow: true,
      noActiveDeployment: true,
    }),
  {
    'the default sort is submitTime descending': async (job, assert) => {
      const mostRecentLaunch = server.db.jobs
        .where({ parentId: job.id })
        .sortBy('submitTime')
        .reverse()[0];

      assert.ok(JobDetail.jobsHeader.hasSubmitTime);
      assert.equal(
        JobDetail.jobs[0].submitTime,
        moment(mostRecentLaunch.submitTime / 1000000).format(
          'MMM DD HH:mm:ss ZZ'
        )
      );
    },
    "don't display redundant information in children table": async function (
      job,
      assert
    ) {
      assert.notOk(JobDetail.jobsHeader.hasNodePool);
      assert.notOk(JobDetail.jobsHeader.hasPriority);
      assert.notOk(JobDetail.jobsHeader.hasType);
    },
  }
);

moduleForJob(
  'Acceptance | job detail (parameterized in namespace)',
  'children',
  () => {
    const namespace = server.create('namespace', { id: 'test' });
    const parent = server.create('job', 'parameterized', {
      shallow: true,
      namespaceId: namespace.name,
    });
    return parent;
  },
  {
    "don't display namespace in children table": async function (job, assert) {
      assert.notOk(JobDetail.jobsHeader.hasNamespace);
    },
  }
);

moduleForJob('Acceptance | job detail (periodic child)', 'allocations', () => {
  const parent = server.create('job', 'periodic', {
    childrenCount: 1,
    shallow: true,
    createAllocations: true,
    allocStatusDistribution: {
      running: 1,
    },
    noActiveDeployment: true,
  });
  return server.db.jobs.where({ parentId: parent.id })[0];
});

moduleForJob(
  'Acceptance | job detail (parameterized child)',
  'allocations',
  () => {
    const parent = server.create('job', 'parameterized', {
      childrenCount: 1,
      shallow: true,
      noActiveDeployment: true,
      createAllocations: true,
      allocStatusDistribution: {
        running: 1,
      },
    });
    return server.db.jobs.where({ parentId: parent.id })[0];
  }
);

moduleForJob(
  'Acceptance | job detail (service)',
  'allocations',
  () => server.create('job', { type: 'service', noActiveDeployment: true }),
  {
    'the subnav links to deployment': async (job, assert) => {
      await JobDetail.tabFor('deployments').visit();
      assert.equal(currentURL(), `/jobs/${job.id}/deployments`);
    },
    'when the job is not found, an error message is shown, but the URL persists':
      async (job, assert) => {
        await JobDetail.visit({ id: 'not-a-real-job' });

        assert.equal(
          server.pretender.handledRequests
            .filter((request) => !request.url.includes('policy'))
            .findBy('status', 404).url,
          '/v1/job/not-a-real-job',
          'A request to the nonexistent job is made'
        );
        assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists');
        assert.ok(JobDetail.error.isPresent, 'Error message is shown');
        assert.equal(
          JobDetail.error.title,
          'Not Found',
          'Error message is for 404'
        );
      },
  }
);

module('Acceptance | job detail (with namespaces)', function (hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  let job, managementToken, clientToken;

  hooks.beforeEach(function () {
    server.createList('namespace', 2);
    server.create('node-pool');
    server.create('node');
    job = server.create('job', {
      type: 'service',
      status: 'running',
      namespaceId: server.db.namespaces[1].name,
      noActiveDeployment: true,
    });
    server.createList('job', 3, {
      namespaceId: server.db.namespaces[0].name,
    });

    managementToken = server.create('token');
    clientToken = server.create('token');
  });

  test('it passes an accessibility audit', async function (assert) {
    const namespace = server.db.namespaces.find(job.namespaceId);
    await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
    await a11yAudit(assert);
  });

  test('when there are namespaces, the job detail page states the namespace for the job', async function (assert) {
    const namespace = server.db.namespaces.find(job.namespaceId);

    await JobDetail.visit({
      id: `${job.id}@${namespace.name}`,
    });

    assert.ok(
      JobDetail.statFor('namespace').text,
      'Namespace included in stats'
    );
  });

  test('the exec button state can change between namespaces', async function (assert) {
    const job1 = server.create('job', {
      status: 'running',
      namespaceId: server.db.namespaces[0].id,
    });
    const job2 = server.create('job', {
      status: 'running',
      namespaceId: server.db.namespaces[1].id,
    });

    window.localStorage.nomadTokenSecret = clientToken.secretId;

    const policy = server.create('policy', {
      id: 'something',
      name: 'something',
      rulesJSON: {
        Namespaces: [
          {
            Name: job1.namespaceId,
            Capabilities: ['list-jobs', 'alloc-exec'],
          },
          {
            Name: job2.namespaceId,
            Capabilities: ['list-jobs'],
          },
        ],
      },
    });

    clientToken.policyIds = [policy.id];
    clientToken.save();

    await JobDetail.visit({ id: job1.id });
    assert.notOk(JobDetail.execButton.isDisabled);

    const secondNamespace = server.db.namespaces[1];
    await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });

    assert.ok(JobDetail.execButton.isDisabled);
  });

  test('the anonymous policy is fetched to check whether to show the exec button', async function (assert) {
    window.localStorage.removeItem('nomadTokenSecret');

    server.create('policy', {
      id: 'anonymous',
      name: 'anonymous',
      rulesJSON: {
        Namespaces: [
          {
            Name: 'default',
            Capabilities: ['list-jobs', 'alloc-exec'],
          },
        ],
      },
    });

    await JobDetail.visit({
      id: `${job.id}@${server.db.namespaces[1].name}`,
    });

    assert.notOk(JobDetail.execButton.isDisabled);
  });

  test('meta table is displayed if job has meta attributes', async function (assert) {
    const jobWithMeta = server.create('job', {
      status: 'running',
      namespaceId: server.db.namespaces[1].id,
      meta: {
        'a.b': 'c',
      },
    });

    await JobDetail.visit({
      id: `${job.id}@${server.db.namespaces[1].name}`,
    });

    assert.notOk(JobDetail.metaTable, 'Meta table not present');

    await JobDetail.visit({
      id: `${jobWithMeta.id}@${server.db.namespaces[1].name}`,
    });
    assert.ok(JobDetail.metaTable, 'Meta table is present');
  });

  test('pack details are displayed', async function (assert) {
    const namespace = server.db.namespaces[1].id;
    const jobFromPack = server.create('job', {
      status: 'running',
      namespaceId: namespace,
      meta: {
        'pack.name': 'my-pack',
        'pack.version': '1.0.0',
      },
    });

    await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` });

    assert.ok(JobDetail.packTag, 'Pack tag is present');
    assert.equal(
      JobDetail.packStatFor('name').text,
      `Name ${jobFromPack.meta['pack.name']}`,
      `Pack name is ${jobFromPack.meta['pack.name']}`
    );
    assert.equal(
      JobDetail.packStatFor('version').text,
      `Version ${jobFromPack.meta['pack.version']}`,
      `Pack version is ${jobFromPack.meta['pack.version']}`
    );
  });

  test('resource recommendations show when they exist and can be expanded, collapsed, and processed', async function (assert) {
    server.create('feature', { name: 'Dynamic Application Sizing' });

    job = server.create('job', {
      type: 'service',
      status: 'running',
      namespaceId: server.db.namespaces[1].name,
      groupsCount: 3,
      createRecommendations: true,
      noActiveDeployment: true,
    });

    window.localStorage.nomadTokenSecret = managementToken.secretId;
    await JobDetail.visit({
      id: `${job.id}@${server.db.namespaces[1].name}`,
    });

    const groupsWithRecommendations = job.taskGroups.filter((group) =>
      group.tasks.models.any((task) => task.recommendations.models.length)
    );
    const jobRecommendationCount = groupsWithRecommendations.length;

    const firstRecommendationGroup = groupsWithRecommendations.models[0];

    assert.equal(JobDetail.recommendations.length, jobRecommendationCount);

    const recommendation = JobDetail.recommendations[0];

    assert.equal(recommendation.group, firstRecommendationGroup.name);
    assert.ok(recommendation.card.isHidden);

    const toggle = recommendation.toggleButton;

    assert.equal(toggle.text, 'Show');

    await toggle.click();

    assert.ok(recommendation.card.isPresent);
    assert.equal(toggle.text, 'Collapse');

    await toggle.click();

    assert.ok(recommendation.card.isHidden);

    await toggle.click();

    assert.equal(
      recommendation.card.slug.groupName,
      firstRecommendationGroup.name
    );

    await recommendation.card.acceptButton.click();

    assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1);

    await JobDetail.tabFor('definition').visit();
    await JobDetail.tabFor('overview').visit();

    assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1);
  });

  test('resource recommendations are not fetched when the feature doesn’t exist', async function (assert) {
    window.localStorage.nomadTokenSecret = managementToken.secretId;
    await JobDetail.visit({
      id: `${job.id}@${server.db.namespaces[1].name}`,
    });

    assert.equal(JobDetail.recommendations.length, 0);

    assert.equal(
      server.pretender.handledRequests.filter((request) =>
        request.url.includes('recommendations')
      ).length,
      0
    );
  });

  test('when the dynamic autoscaler is applied, you can scale a task within the job detail page', async function (assert) {
    const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace';
    const READ_ONLY_NAMESPACE = 'read-only-namespace';
    const clientToken = server.create('token');

    const namespace = server.create('namespace', {
      id: SCALE_AND_WRITE_NAMESPACE,
    });
    const secondNamespace = server.create('namespace', {
      id: READ_ONLY_NAMESPACE,
    });

    job = server.create('job', {
      groupCount: 0,
      createAllocations: false,
      shallow: true,
      noActiveDeployment: true,
      namespaceId: SCALE_AND_WRITE_NAMESPACE,
    });

    const job2 = server.create('job', {
      groupCount: 0,
      createAllocations: false,
      shallow: true,
      noActiveDeployment: true,
      namespaceId: READ_ONLY_NAMESPACE,
    });
    const scalingGroup2 = server.create('task-group', {
      job: job2,
      name: 'scaling',
      count: 1,
      shallow: true,
      withScaling: true,
    });
    job2.update({ taskGroupIds: [scalingGroup2.id] });

    const policy = server.create('policy', {
      id: 'something',
      name: 'something',
      rulesJSON: {
        Namespaces: [
          {
            Name: SCALE_AND_WRITE_NAMESPACE,
            Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
          },
          {
            Name: READ_ONLY_NAMESPACE,
            Capabilities: ['list-jobs', 'read-job'],
          },
        ],
      },
    });
    const scalingGroup = server.create('task-group', {
      job,
      name: 'scaling',
      count: 1,
      shallow: true,
      withScaling: true,
    });
    job.update({ taskGroupIds: [scalingGroup.id] });

    clientToken.policyIds = [policy.id];
    clientToken.save();
    window.localStorage.nomadTokenSecret = clientToken.secretId;

    await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
    assert.notOk(JobDetail.incrementButton.isDisabled);

    await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
    assert.ok(JobDetail.incrementButton.isDisabled);
  });

  test('handles when a job is remotely purged', async function (assert) {
    const namespace = server.create('namespace');
    const job = server.create('job', {
      namespaceId: namespace.id,
      status: 'running',
      type: 'service',
      shallow: true,
      noActiveDeployment: true,
      createAllocations: true,
      groupsCount: 1,
      groupTaskCount: 1,
      allocStatusDistribution: {
        running: 1,
      },
    });

    await JobDetail.visit({ id: `${job.id}@${namespace.id}` });

    assert.equal(currentURL(), `/jobs/${job.id}%40${namespace.id}`);

    // Simulate a 404 error on the job watcher
    const controller = this.owner.lookup('controller:jobs.job');
    let jobWatcher = controller.watchers.job;
    jobWatcher.isError = true;
    jobWatcher.error = { errors: [{ status: '404' }] };
    await settled();

    // User should be booted off the page
    assert.equal(currentURL(), '/jobs?namespace=*');

    // A notification should be present
    assert
      .dom('.flash-message.alert-critical')
      .exists('A toast error message pops up.');

    await percySnapshot(assert);
  });

  test('handles when a job is remotely purged, from a job subnav page', async function (assert) {
    const namespace = server.create('namespace');
    const job = server.create('job', {
      namespaceId: namespace.id,
      status: 'running',
      type: 'service',
      shallow: true,
      noActiveDeployment: true,
      createAllocations: true,
      groupsCount: 1,
      groupTaskCount: 1,
      allocStatusDistribution: {
        running: 1,
      },
    });

    await JobDetail.visit({ id: `${job.id}@${namespace.id}` });
    await JobDetail.tabFor('allocations').visit();

    assert.equal(currentURL(), `/jobs/${job.id}@${namespace.id}/allocations`);

    // Simulate a 404 error on the job watcher
    const controller = this.owner.lookup('controller:jobs.job');
    let jobWatcher = controller.watchers.job;
    jobWatcher.isError = true;
    jobWatcher.error = { errors: [{ status: '404' }] };
    await settled();

    // User should be booted off the page
    assert.equal(currentURL(), '/jobs?namespace=*');

    // A notification should be present
    assert
      .dom('.flash-message.alert-critical')
      .exists('A toast error message pops up.');
  });
});
