/*
 * Copyright (C) Sapphirecode - All Rights Reserved
 * This file is part of Auth-Server-Helper which is released under MIT.
 * See file 'LICENSE' for full license details.
 * Created by Timo Hocker <timo@scode.ovh>, January 2021
 */

import http, { IncomingMessage, ServerResponse } from 'http';
import { to_b64 } from '@sapphirecode/encoding-helper';
import auth from '../../lib/Authority';
import { get, modify_signature, Response } from '../Helper';
import create_auth_handler from '../../lib/AuthHandler';


const expires_seconds = 600;
const refresh_expires_seconds = 3600;
const part_expires_seconds = 60;

interface CheckHeaderResult {
  at: string;
  rt?: string;
  data: Record<string, unknown>;
}

function check_headers (resp: Response): CheckHeaderResult {
  const data = JSON.parse (resp.body as string);
  const at = data.access_token;
  const rt = data.refresh_token;
  expect (resp.headers['cache-control'])
    .toEqual ('no-store');
  expect (resp.headers.pragma)
    .toEqual ('no-cache');
  return { data, at, rt };
}

function check_token (token: string, type: string):void {
  const v = auth.verify (token);
  expect (v.valid)
    .toEqual (true);
  expect (v.authorized)
    .toEqual (type === 'access_token');
  expect (v.type)
    .toEqual (type);
  expect (token)
    .toMatch (/^[0-9a-z-._~+/]+$/ui);
}

// eslint-disable-next-line max-lines-per-function
describe ('auth handler', () => {
  let server: http.Server|null = null;
  beforeAll (() => {
    const ah = create_auth_handler ((req) => {
      if (!req.is_basic) {
        req.invalid ('unknown autorization type');
      }
      else if (req.user === 'foo' && req.password === 'bar') {
        req.allow_access ({
          access_token_expires_in:  expires_seconds,
          include_refresh_token:    true,
          refresh_token_expires_in: refresh_expires_seconds
        });
      }
      else if (req.user === 'part' && req.password === 'bar') {
        req.allow_part (part_expires_seconds, 'two_factor');
      }
      else {
        req.deny ();
      }
    }, {
      cookie_name: 'cookie_jar',
      refresh:     {
        access_token_expires_in:  expires_seconds,
        refresh_token_expires_in: refresh_expires_seconds,
        include_refresh_token:    true
      },
      modules: {
        two_factor (request) {
          if (request.body === 'letmein') {
            request.allow_access ({
              access_token_expires_in:  expires_seconds,
              include_refresh_token:    true,
              refresh_token_expires_in: refresh_expires_seconds
            });
          }
          else { request.deny (); }
        }
      }
    });

    server = http.createServer ((req: IncomingMessage, res: ServerResponse) => {
      ah (req, res);
    });
    server.listen (3000);

    jasmine.clock ()
      .install ();
    jasmine.clock ()
      .mockDate (new Date);
  });

  it ('auth test sequence', async () => {
    // get initial access and refresh tokens
    const resp1 = await get ({ authorization: 'Basic foo:bar' });
    expect (resp1.statusCode)
      .toEqual (200);
    const res1 = check_headers (resp1);
    expect (res1.data.token_type)
      .toEqual ('bearer');
    expect (resp1.headers['set-cookie'])
      .toContain (`cookie_jar=${res1.at}`);

    check_token (res1.at as string, 'access_token');
    expect (res1.data.expires_in)
      .toEqual (expires_seconds);

    check_token (res1.rt as string, 'refresh_token');
    expect (res1.data.refresh_expires_in)
      .toEqual (refresh_expires_seconds);


    // get refresh token
    const resp2 = await get ({ authorization: `Bearer ${res1.rt}` });
    expect (resp2.statusCode)
      .toEqual (200);
    const res2 = check_headers (resp2);
    expect (res2.data.token_type)
      .toEqual ('bearer');
    expect (resp2.headers['set-cookie'])
      .toContain (`cookie_jar=${res2.at}`);

    check_token (res2.at as string, 'access_token');
    expect (res2.data.expires_in)
      .toEqual (expires_seconds);
    expect (res2.at).not.toEqual (res1.at);

    check_token (res2.rt as string, 'refresh_token');
    expect (res2.data.refresh_expires_in)
      .toEqual (refresh_expires_seconds);
    expect (res2.rt).not.toEqual (res1.rt);
  });

  it ('should return the correct denial message', async () => {
    const resp = await get ({ authorization: 'Basic bar:baz' });
    expect (resp.statusCode)
      .toEqual (401);
    const res = check_headers (resp);
    expect (res.data)
      .toEqual ({ error: 'invalid_client' });
  });

  it ('should allow base64 login', async () => {
    // get initial access and refresh tokens
    const resp1 = await get ({ authorization: `Basic ${to_b64 ('foo:bar')}` });
    expect (resp1.statusCode)
      .toEqual (200);
    const res1 = check_headers (resp1);
    expect (res1.data.token_type)
      .toEqual ('bearer');
    expect (resp1.headers['set-cookie'])
      .toContain (`cookie_jar=${res1.at}`);

    check_token (res1.at as string, 'access_token');
    expect (res1.data.expires_in)
      .toEqual (expires_seconds);

    check_token (res1.rt as string, 'refresh_token');
    expect (res1.data.refresh_expires_in)
      .toEqual (refresh_expires_seconds);
  });

  it ('should reject invalid requests', async () => {
    const resp1 = await get ();
    expect (resp1.statusCode)
      .toEqual (401);
    const res1 = check_headers (resp1);
    expect (res1.data)
      .toEqual ({ error: 'invalid_client' });

    const resp2a = await get ({ authorization: 'Basic foo:bar' });
    const res2a = check_headers (resp2a);
    const resp2b = await get (
      { authorization: `Bearer ${res2a.at}` }
    );
    expect (resp2b.statusCode)
      .toEqual (400);
    const res2 = check_headers (resp2b);
    expect (res2.data)
      .toEqual ({
        error:             'invalid_request',
        error_description: 'invalid bearer type'
      });
  });

  it ('should reject an invalid token', async () => {
    const resp1 = await get ({ authorization: 'Basic foo:bar' });
    const res1 = check_headers (resp1);
    const resp2 = await get (
      { authorization: `Bearer ${modify_signature (res1.at)}` }
    );
    expect (resp2.statusCode)
      .toEqual (401);
    const res2 = check_headers (resp2);
    expect (res2.data)
      .toEqual ({ error: 'invalid_client' });
  });

  it ('should process part token', async () => {
    const resp1 = await get ({ authorization: 'Basic part:bar' });
    expect (resp1.statusCode)
      .toEqual (200);
    const res1 = check_headers (resp1);
    expect (res1.data.token_type)
      .toEqual ('bearer');
    expect (res1.data.expires_in)
      .toEqual (part_expires_seconds);
    check_token (res1.data.part_token as string, 'part_token');

    const resp2 = await get (
      { authorization: `Bearer ${res1.data.part_token}` },
      'letmein'
    );
    expect (resp2.statusCode)
      .toEqual (200);
    const res2 = check_headers (resp2);
    expect (res2.data.token_type)
      .toEqual ('bearer');
    expect (resp2.headers['set-cookie'])
      .toContain (`cookie_jar=${res2.at}`);

    check_token (res2.at as string, 'access_token');
    expect (res2.data.expires_in)
      .toEqual (expires_seconds);
    expect (res2.at).not.toEqual (res1.at);

    check_token (res2.rt as string, 'refresh_token');
    expect (res2.data.refresh_expires_in)
      .toEqual (refresh_expires_seconds);
    expect (res2.rt).not.toEqual (res1.rt);
  });

  afterAll (() => {
    if (server === null)
      throw new Error ('server is null');
    server.close ();
    jasmine.clock ()
      .tick (24 * 60 * 60 * 1000);
    jasmine.clock ()
      .uninstall ();
  });
});