/*
 * Copyright 2017 Red Hat Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
'use strict';

const admin = require('./utils/realm');
const NodeApp = require('./fixtures/node-console/index').NodeApp;
const TestVector = require('./utils/helper').TestVector;

const test = require('blue-tape');
const axios = require('axios');
const getToken = require('./utils/token');

const realmName = 'mixed-mode-realm';
const realmManager = admin.createRealm(realmName);
const app = new NodeApp();

const auth = {
  username: 'test-admin',
  password: 'password'
};

const getSessionCookie = response => response.headers['set-cookie'][0].split(';')[0];

const assertAlternativeMessages = (assert, message, ...messages) => {
  assert.true(messages.includes(message), `Message "${message}" is not included in provided messages`);
};

test('setup', t => {
  return realmManager.then(() => {
    return admin.createClient(app.confidential(), realmName)
      .then((installation) => {
        return app.build(installation);
      });
  });
});

test('Should test protected route.', t => {
  t.plan(1);
  const opt = {
    url: `${app.address}/service/admin`
  };
  return t.shouldFail(axios(opt), 'Access denied', 'Response should be access denied for no credentials');
});

test('Should test protected route with admin credentials.', t => {
  t.plan(1);
  return getToken({ realmName }).then((token) => {
    const opt = {
      url: `${app.address}/service/admin`,
      headers: { Authorization: `Bearer ${token}` }
    };
    return axios(opt)
      .then(response => {
        t.equal(response.data.message, 'admin');
      })
      .catch(error => {
        t.fail(error.response.data);
      });
  });
});

test('Should test protected route with invalid access token.', t => {
  t.plan(1);
  return getToken({ realmName }).then((token) => {
    const opt = {
      url: `${app.address}/service/admin`,
      headers: {
        Authorization: 'Bearer ' + token.replace(/(.+?\..+?\.).*/, '$1.Invalid')
      }
    };
    return t.shouldFail(axios(opt), 'Access denied', 'Response should be access denied for invalid access token');
  });
});

test('Should handle direct access grants.', t => {
  t.plan(3);
  return axios.post(`${app.address}/service/grant`, auth)
    .then(response => {
      t.ok(response.data.id_token, 'Response should contain an id_token');
      t.ok(response.data.access_token, 'Response should contain an access_token');
      t.ok(response.data.refresh_token, 'Response should contain an refresh_token');
    })
    .catch(error => {
      t.fail(error.response.data);
    });
});

test('Should store the grant.', t => {
  t.plan(3);
  const endpoint = `${app.address}/service/grant`;
  return axios.post(endpoint, auth)
    .then(response => getSessionCookie(response))
    .then(cookie => {
      return axios.get(endpoint, { headers: { cookie } })
        .then(response => {
          t.ok(response.data.id_token, 'Response should contain an id_token');
          t.ok(response.data.access_token, 'Response should contain an access_token');
          t.ok(response.data.refresh_token, 'Response should contain an refresh_token');
        });
    });
});

test('Should not store grant on bearer request', t => {
  t.plan(4);
  const endpoint = `${app.address}/service/grant`;
  let sessionCookie;

  return axios.post(endpoint, auth)
    .then(response => {
      const data = {
        cookie: getSessionCookie(response),
        grant: response.data
      };
      sessionCookie = data.cookie;
      return data;
    })
    .then(data => {
      const opt = {
        url: `${app.address}/service/secured`,
        headers: {
          Authorization: 'Bearer ' + data.grant.access_token.token,
          Cookie: data.cookie
        }
      };

      return axios(opt)
        .then(response => {
          t.equal(response.data.message, 'secured');

          const opt = {
            url: endpoint,
            headers: {
              Cookie: sessionCookie
            }
          };

          return axios(opt)
            .then(response => {
              t.ok(response.data.id_token, 'Response should contain an id_token');
              t.ok(response.data.access_token, 'Response should contain an access_token');
              t.ok(response.data.refresh_token, 'Response should contain an refresh_token');
            });
        });
    });
});

test('Should test admin logout endpoint with incomplete payload', t => {
  t.plan(2);

  const app = new NodeApp();
  const client = admin.createClient(app.confidential('adminapp'), realmName);

  return client.then((installation) => {
    app.build(installation);

    const opt = {
      method: 'post',
      url: `${app.address}/k_logout`,
      data: TestVector.logoutIncompletePayload
    };
    return axios(opt).catch(err => {
      t.equal(err.response.status, 401);

      assertAlternativeMessages(t, err.response.data,
        "Cannot read property 'kid' of undefined",
        "Cannot read properties of undefined (reading 'kid')"
      );

      app.destroy();
    });
  }).then(() => {
    app.destroy();
  });
});

test('Should test admin logout endpoint with payload signed by a different key pair', t => {
  t.plan(2);

  const app = new NodeApp();
  const client = admin.createClient(app.confidential('adminapp2'), realmName);

  return client.then((installation) => {
    app.build(installation);

    const opt = {
      method: 'post',
      url: `${app.address}/k_logout`,
      data: TestVector.logoutWrongKeyPairPayload
    };
    return axios(opt).catch(err => {
      t.equal(err.response.status, 401);
      t.equal(err.response.data, 'admin request failed: invalid token (signature)');
      app.destroy();
    });
  }).then(() => {
    app.destroy();
  });
});

test('Should test admin logout endpoint with valid payload', t => {
  t.plan(1);

  const app = new NodeApp();
  const client = admin.createClient(app.confidential('adminapp3'), realmName);

  return client.then((installation) => {
    app.build(installation);
    const opt = {
      method: 'post',
      url: `${app.address}/k_logout`,
      data: TestVector.logoutValidPayload
    };
    return axios(opt).then(response => {
      t.equal(response.status, 200);
    }).catch(err => {
      t.fail(err.response.data);
    });
  }).then(() => {
    app.destroy();
  });
});

test('Should test admin push_not_before endpoint with incomplete payload', t => {
  t.plan(2);

  const app = new NodeApp();
  const client = admin.createClient(app.confidential('adminapp5'), realmName);

  return client.then((installation) => {
    app.build(installation);

    const opt = {
      method: 'post',
      url: `${app.address}/k_push_not_before`,
      data: TestVector.notBeforeIncompletePayload
    };
    return axios(opt).catch(err => {
      t.equal(err.response.status, 401);

      assertAlternativeMessages(t, err.response.data,
        "Cannot read property 'kid' of undefined",
        "Cannot read properties of undefined (reading 'kid')"
      );

      app.destroy();
    });
  }).then(() => {
    app.destroy();
  });
});

test('Should test admin push_not_before endpoint with payload signed by a different key pair', t => {
  t.plan(2);

  const app = new NodeApp();
  const client = admin.createClient(app.confidential('adminapp6'), realmName);

  return client.then((installation) => {
    app.build(installation);

    const opt = {
      method: 'post',
      url: `${app.address}/k_push_not_before`,
      data: TestVector.notBeforeWrongKeyPairPayload
    };
    return axios(opt).catch(err => {
      t.equal(err.response.status, 401);
      t.equal(err.response.data, 'admin request failed: invalid token (signature)');
      app.destroy();
    });
  }).then(() => {
    app.destroy();
  });
});

test('Should verify during authentication if the token contains the client name as audience.', t => {
  t.plan(3);
  const someapp = new NodeApp();
  const client = admin.createClient(someapp.confidential('audience-app'), realmName);

  return client.then((installation) => {
    installation.verifyTokenAudience = true;
    someapp.build(installation);

    return axios.post(`${someapp.address}/service/grant`, auth)
      .then(response => {
        t.ok(response.data.id_token, 'Response should contain an id_token');
        t.ok(response.data.access_token, 'Response should contain an access_token');
        t.ok(response.data.refresh_token, 'Response should contain an refresh_token');
      })
      .catch(error => {
        t.fail(error.response.data);
      });
  }).then(() => {
    someapp.destroy();
  });
});

test('Should test admin push_not_before endpoint with valid payload', t => {
  t.plan(1);

  const app = new NodeApp();
  const client = admin.createClient(app.confidential('adminapp7'), realmName);

  return client.then((installation) => {
    app.build(installation);
    const opt = {
      method: 'post',
      url: `${app.address}/k_push_not_before`,
      data: TestVector.notBeforeValidPayload
    };
    return axios(opt).then(response => {
      t.equal(response.status, 200);
    }).catch(err => {
      t.fail(err.response.data);
    });
  }).then(() => {
    app.destroy();
  });
});

test.skip('Should logout with redirect url', t => {
  t.plan(1);
  const serviceEndpoint = `${app.address}/service/grant`;
  const logoutEndpoint = `${app.address}/logout?redirect_url=http%3A%2F%2Flocalhost%3A${app.port}%2Fbye`;
  return axios.post(serviceEndpoint, auth)
    .then(response => getSessionCookie(response))
    .then(cookie => {
      return axios.get(logoutEndpoint, { headers: { cookie } })
        .then(response => {
          t.assert(response.request.path, '/bye', 'Expected redirect after logout');
        });
    });
});

test('teardown', t => {
  return realmManager.then((realm) => {
    app.destroy();
    admin.destroy(realmName);
  });
});
