/**
 * Copyright IBM Corp. 2016, 2025
 * SPDX-License-Identifier: BUSL-1.1
 */

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, fillIn, find, findAll, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
import { AUTH_METHOD_LOGIN_DATA } from 'vault/tests/helpers/auth/auth-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { ENTERPRISE_LOGIN_METHODS, ERROR_JWT_LOGIN, supportedTypes } from 'vault/utils/auth-form-helpers';
import { overrideResponse } from 'vault/tests/helpers/stubs';
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';

module('Integration | Component | auth | form template', function (hooks) {
  setupRenderingTest(hooks);
  setupMirage(hooks);

  hooks.beforeEach(function () {
    window.localStorage.clear();
    this.version = this.owner.lookup('service:version');
    this.router = this.owner.lookup('service:router');
    this.cluster = { id: '1' };

    this.alternateView = null;
    this.defaultView = { view: 'dropdown', tabData: null };
    this.initialFormState = { initialAuthType: 'token', showAlternate: false };
    this.handleAuthResponse = sinon.spy();
    this.visibleMountTypes = null;

    this.renderComponent = () => {
      return render(hbs`
         <Auth::FormTemplate
          @alternateView={{this.alternateView}}
          @cluster={{this.cluster}}
          @defaultView={{this.defaultView}}
          @initialFormState={{this.initialFormState}}
          @handleAuthResponse={{this.handleAuthResponse}}
          @visibleMountTypes={{this.visibleMountTypes}}
        />`);
    };
  });

  // test to select each method is in "ent" module to include enterprise methods
  test('it selects token by default', async function (assert) {
    await this.renderComponent();
    assert.dom(GENERAL.selectByAttr('auth type')).hasValue('token');
  });

  test('it does not show toggle buttons if @alternateView does not exist', async function (assert) {
    await this.renderComponent();
    assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render');
    assert
      .dom(GENERAL.button('Sign in with other methods'))
      .doesNotExist('"Sign in with other methods" does not render');
  });

  test('it initializes with preset auth type', async function (assert) {
    this.initialFormState = { initialAuthType: 'userpass' };
    await this.renderComponent();
    assert.dom(GENERAL.selectByAttr('auth type')).hasValue('userpass');
  });

  test('it displays errors', async function (assert) {
    const api = this.owner.lookup('service:api');
    // stub auth request for "token" method because it's selected by default
    const tokenLookUpSelfStub = sinon.stub(api.auth, 'tokenLookUpSelf');
    tokenLookUpSelfStub.rejects(getErrorResponse({ errors: ['uh oh!'] }, 400));
    await this.renderComponent();
    await click(GENERAL.submitButton);
    assert.dom(GENERAL.messageError).hasText('Error Authentication failed: uh oh!');
    tokenLookUpSelfStub.restore();
  });

  test('dropdown does not include enterprise methods on community versions', async function (assert) {
    this.version.type = 'community';
    const supported = supportedTypes(false);
    const unsupported = ENTERPRISE_LOGIN_METHODS.map((m) => m.type);
    assert.expect(supported.length + unsupported.length);
    await this.renderComponent();
    const dropdownOptions = findAll(`${GENERAL.selectByAttr('auth type')} option`).map((o) => o.value);

    supported.forEach((m) => {
      assert.true(dropdownOptions.includes(m), `dropdown includes supported method: ${m}`);
    });
    unsupported.forEach((m) => {
      assert.false(dropdownOptions.includes(m), `dropdown does NOT include unsupported method: ${m}`);
    });
  });

  module('listing visibility', function (hooks) {
    hooks.beforeEach(function () {
      const defaultTabs = {
        userpass: [
          {
            path: 'userpass/',
            description: '',
            options: {},
            type: 'userpass',
          },
          {
            path: 'userpass2/',
            description: '',
            options: {},
            type: 'userpass',
          },
        ],
        ldap: [
          {
            path: 'my-ldap/',
            description: '',
            options: {},
            type: 'ldap',
          },
        ],
        token: [
          {
            path: 'token/',
            description: 'token based credentials',
            options: null,
            type: 'token',
          },
        ],
      };
      // all computed by the parent, in this case the initial tabs are the same as visible mount types
      // but that isn't always the case
      this.visibleMountTypes = Object.keys(defaultTabs);
      this.defaultView = { type: 'tabs', tabData: defaultTabs };
      this.alternateView = { type: 'dropdown', tabData: null };
      this.initialFormState = { initialAuthType: 'userpass', showAlternate: false };
    });

    test('it selects each auth tab and renders form for that type', async function (assert) {
      await this.renderComponent();
      const assertSelected = (type) => {
        assert.dom(AUTH_FORM.authForm(type)).exists(`${type}: form renders when tab is selected`);
        assert.dom(AUTH_FORM.tabBtn(type)).hasAttribute('aria-selected', 'true');
      };
      const assertUnselected = (type) => {
        assert.dom(AUTH_FORM.authForm(type)).doesNotExist(`${type}: form does NOT render`);
        assert.dom(AUTH_FORM.tabBtn(type)).hasAttribute('aria-selected', 'false');
      };
      // click through each tab
      await click(AUTH_FORM.tabBtn('userpass'));
      assertSelected('userpass');
      assertUnselected('ldap');
      assertUnselected('token');
      assert.dom(AUTH_FORM.advancedSettings).doesNotExist();

      await click(AUTH_FORM.tabBtn('ldap'));
      assertSelected('ldap');
      assertUnselected('token');
      assertUnselected('userpass');
      assert.dom(AUTH_FORM.advancedSettings).doesNotExist();

      await click(AUTH_FORM.tabBtn('token'));
      assertSelected('token');
      assertUnselected('ldap');
      assertUnselected('userpass');
      assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
    });

    test('it clicks "Sign in with other methods" and toggles to other view', async function (assert) {
      await this.renderComponent();
      assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'tabs render by default');
      assert.dom(GENERAL.backButton).doesNotExist();
      await click(GENERAL.button('Sign in with other methods'));
      assert
        .dom(GENERAL.button('Sign in with other methods'))
        .doesNotExist('"Sign in with other methods" does not render after it is clicked');
      assert
        .dom(GENERAL.selectByAttr('auth type'))
        .exists('clicking "Sign in with other methods" renders dropdown instead of tabs');
      await click(GENERAL.backButton);
      assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render after it is clicked');
      assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'clicking "Back" renders tabs again');
      assert
        .dom(GENERAL.button('Sign in with other methods'))
        .exists('"Sign in with other methods" renders again');
    });

    test('it resets selected tab after clicking "Sign in with other methods" and then "Back"', async function (assert) {
      await this.renderComponent();
      assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
      assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'false');
      assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'false');

      // select a different tab before clicking "Sign in with other methods"
      await click(AUTH_FORM.tabBtn('ldap'));
      assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'true');
      assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'false');
      await click(GENERAL.button('Sign in with other methods'));
      assert.dom(GENERAL.selectByAttr('auth type')).exists('it renders dropdown instead of tabs');
      await click(GENERAL.backButton);
      // assert tab selection is reset
      assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
      assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'false');
      assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'false');
    });

    test('it preselects tab from initialFormState', async function (assert) {
      this.initialFormState = { initialAuthType: 'ldap', showAlternate: false };
      await this.renderComponent();
      assert.dom(AUTH_FORM.authForm('ldap')).exists('ldap form renders');
      assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'true');
    });

    test('it renders dropdown and preselects type if initialFormState is not a tab', async function (assert) {
      this.initialFormState = { initialAuthType: 'okta', showAlternate: true };
      await this.renderComponent();
      assert.dom(GENERAL.selectByAttr('auth type')).hasValue('okta');
      assert.dom(GENERAL.inputByAttr('username')).exists();
      assert.dom(GENERAL.inputByAttr('password')).exists();

      assert.dom(GENERAL.backButton).exists('"Back" button renders');
      assert
        .dom(GENERAL.button('Sign in with other methods'))
        .doesNotExist('"Sign in with other methods" does not render');
    });
  });

  // tests with "enterprise" in the title are filtered out from CE test runs
  // naming the module 'ent' so these tests still run on the CE repo
  module('ent', function (hooks) {
    hooks.beforeEach(function () {
      this.version.type = 'enterprise';
      this.namespaceQueryParam = '';
    });

    // in the ent module to test ALL supported login methods
    // iterating in tests should generally be avoided, but purposefully wanted to test the component
    // renders as expected as auth types change
    test('it selects each supported auth type and renders its form and relevant fields', async function (assert) {
      const authMethodTypes = supportedTypes(true);
      const totalFields = Object.values(AUTH_METHOD_LOGIN_DATA).reduce(
        (sum, obj) => sum + Object.keys(obj).length,
        0
      );
      // 3 assertions per method, plus an assertion for each expected field
      assert.expect(3 * authMethodTypes.length + totalFields); // count at time of writing is 40

      await this.renderComponent();
      for (const authType of authMethodTypes) {
        let stub;
        if (['oidc', 'jwt'].includes(authType)) {
          stub = sinon.stub(this.router, 'urlFor').returns('123-example.com');
        }
        const loginData = AUTH_METHOD_LOGIN_DATA[authType];

        const fields = Object.keys(loginData);
        await fillIn(GENERAL.selectByAttr('auth type'), authType);

        assert.dom(GENERAL.selectByAttr('auth type')).hasValue(authType), `${authType}: it selects type`;
        assert.dom(AUTH_FORM.authForm(authType)).exists(`${authType}: it renders form component`);

        // token is the only method that does not support a custom mount path
        if (authType !== 'token') {
          // jwt and oidc render the same component so the toggle remains open switching between those types
          const element = find(AUTH_FORM.advancedSettings);
          if (element.ariaExpanded === 'false') {
            await click(AUTH_FORM.advancedSettings);
          }
        }

        const assertion = authType === 'token' ? 'doesNotExist' : 'exists';
        assert.dom(GENERAL.inputByAttr('path'))[assertion](`${authType}: mount path input ${assertion}`);

        fields.forEach((field) => {
          assert.dom(GENERAL.inputByAttr(field)).exists(`${authType}: ${field} input renders`);
        });

        if (stub) {
          stub.restore();
        }
      }
    });

    test('dropdown includes enterprise methods', async function (assert) {
      const supported = supportedTypes(true);
      assert.expect(supported.length);
      await this.renderComponent();

      const dropdownOptions = findAll(`${GENERAL.selectByAttr('auth type')} option`).map((o) => o.value);
      supported.forEach((m) => {
        assert.true(dropdownOptions.includes(m), `dropdown includes supported method: ${m}`);
      });
    });
  });

  // AUTH METHOD SPECIFIC TESTS
  // since the template yields each auth <form> some assertions are best done here instead of
  // in the corresponding the Auth::Form::<Type> integration tests
  module('oidc-jwt', function (hooks) {
    hooks.beforeEach(async function () {
      this.routerStub = (path) => sinon.stub(this.router, 'urlFor').returns(`/auth/${path}/oidc/callback`);
    });

    test('it re-requests the auth_url when authType changes', async function (assert) {
      this.routerStub('oidc');
      assert.expect(2); // auth_url should be hit twice, one for each type selection
      let expectedType = 'oidc';
      this.server.post(`/auth/:path/oidc/auth_url`, (_, req) => {
        assert.strictEqual(
          req.params.path,
          expectedType,
          `it makes request to auth_url for selected type: ${expectedType}`
        );
        return { data: { auth_url: '123-example.com' } };
      });
      await this.renderComponent();
      // auth_url should be requested once when "oidc" is selected
      await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
      // auth_url should be requested again when "jwt" is selected
      expectedType = 'jwt';
      await fillIn(GENERAL.selectByAttr('auth type'), 'jwt');
    });

    // for simplicity the auth types are configured as their namesake but type isn't relevant.
    // these tests assert that CONFIG changes from OIDC -> JWT render correctly and vice versa
    // so the order the requests are hit is what matters.
    test('"OIDC" to "JWT" configuration: it updates the form when the auth_url response changes', async function (assert) {
      this.routerStub('oidc');
      this.server.post(`/auth/oidc/oidc/auth_url`, () => ({ data: { auth_url: '123-example.com' } })); // this return means mount is configured as oidc
      this.server.post(`/auth/jwt/oidc/auth_url`, () => overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })); // this return means the mount is configured as jwt
      await this.renderComponent();

      // select mount configured for OIDC first
      await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
      assert.dom(GENERAL.inputByAttr('jwt')).doesNotExist();
      // then select mount configured for JWT
      await fillIn(GENERAL.selectByAttr('auth type'), 'jwt');
      assert.dom(GENERAL.inputByAttr('jwt')).exists();
    });

    test('"JWT" to "OIDC" configuration: it updates the form when the auth_url response changes', async function (assert) {
      this.routerStub('oidc');
      this.server.post(`/auth/jwt/oidc/auth_url`, () => overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })); // this return means the mount is configured as jwt
      this.server.post(`/auth/oidc/oidc/auth_url`, () => ({ data: { auth_url: '123-example.com' } })); // this return means mount is configured as oidc
      await this.renderComponent();

      // select mount configured for JWT first
      await fillIn(GENERAL.selectByAttr('auth type'), 'jwt');
      assert.dom(GENERAL.inputByAttr('jwt')).exists();

      // then select mount configured for OIDC
      await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
      assert.dom(GENERAL.inputByAttr('jwt')).doesNotExist();
    });

    test('it should retain role input value when mount path changes', async function (assert) {
      assert.expect(2);
      this.routerStub('foo-oidc');
      const auth_url = 'http://dev-foo-bar.com';
      this.server.post('/auth/:path/oidc/auth_url', (_, req) => {
        const { role, redirect_uri } = JSON.parse(req.requestBody);
        const goodRequest =
          req.params.path === 'foo-oidc' &&
          role === 'foo' &&
          redirect_uri.includes('/auth/foo-oidc/oidc/callback');
        if (goodRequest) {
          return { data: { auth_url } };
        } else {
          return overrideResponse(400, { errors: [ERROR_JWT_LOGIN] });
        }
      });

      window.open = (url) => {
        assert.strictEqual(url, auth_url, 'auth_url is returned when required params are passed');
      };

      await this.renderComponent();

      await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
      await fillIn(GENERAL.inputByAttr('role'), 'foo');
      await click(AUTH_FORM.advancedSettings);
      await fillIn(GENERAL.inputByAttr('role'), 'foo');
      await fillIn(GENERAL.inputByAttr('path'), 'foo-oidc');
      assert.dom(GENERAL.inputByAttr('role')).hasValue('foo', 'role is retained when mount path is changed');
      await click(GENERAL.submitButton);
    });
  });
});
