/*
  Helpers for the tests in `tests/` that does things like deal with
  [Playwright](https://playwright.dev) setup, waiting for things in
  /pages, fillng out forms, and making fake users.  For a good
  example of using almost everything look at `tests/ui/registration.js`.

  ### Usage

  I like to pull out each function I use separately, then set everything up like this:

  ```javascript
  import {sleep, expect, tid, playstart, playstop, form, wait} from '../../lib/testing.js';
  import { base_host } from "../../lib/config.js";
  import {knex} from '../../lib/ormish.js';

  test.before(async t => t.context = await playstart(`${base_host}/client/#/register/`));

  test.after(async t => {
    knex.destroy();
    await playstop(t.context.browser, t.context.p);
  });
  ```

  This gets `Playwright` setup to start at the right URL for the test, and makes sure that
  it's stopped with `playstop`.  This is important so that coverage works. You'll also
  notice that I'm manually doing `knex.destroy()`.  If you don't do this then your testing
  runs will hang while Node waits for `knex` to exit.
 */
import playwright from 'playwright';
import faker from 'faker';
import random from 'random';
import assert from "assert";
import { v4 as uuid } from "uuid";
import fs from "fs";
import url from "url";
import path from "path";
import { User } from "./models.js";

const WAIT_TIME = 5000;
const HEADLESS = !process.env.PLAYVIEW;
const COVERAGE = process.env.NODE_V8_COVERAGE;

/*
  Generates the CSS selector to match the `data-testid` for finding HTML
  tags in tests.
 */
export const tid = (name) => `[data-testid="${name}"]`;

/*
  Fills in a form using the id="" of each field, then click
  the button basedon the `tid(button_id)`. For example, here's
  how the stock registration form is tested:

  ```javascript
  await form(p, {
    "#email": user.email,
    "#full_name": user.full_name,
    "#initials": "XXX",
    "#password": user.password,
    "#password_repeat": user.password,
    "#tos_agree": true,
  }, tid("register-button"));
  ```

  __FOOTGUN__: This only works with one form on the page. If you have
  multiple forms then it'll probably fill in fields randomly based on
  the ids.

  + `page Object` -- [Playwright](https://playwright.dev) page object.
  + `fields Object` -- Mapping of "#field-id" to value to type.
  + `button_id string` (___optional___) -- The name of the button to press to submit (or anything).  This is passed to `tid()` so do not include the "#".
 */
export const form = async (page, fields, button_id) => {
  for(const [key, value] of Object.entries(fields)) {
    if(typeof value === "boolean") {
      if(value) {
        await page.check(key);
      } else {
        await page.uncheck(key);
      }
    } else {
      // it's another input to fill
      await page.fill(key, value);
    }
  }

  if(button_id) {
    await page.click(button_id);
  }
}

/*
  Sets up a [Playwright](https://playwright.dev) instance that uses Chromium,
  handles HEADLESS or not, starts at a starting URL, and sets up any coverage
  requirements.

  ### Headless PLAYVIEW

  The code for whether to show the browser window or not is based on if you set
  `PLAYVIEW=1` environment variables then you'll see the browser windows.  This
  makes debugging easier.

  ### Coverage

  Coverage is determined by whether you set `NODE_V8_COVERAGE=.coverage` environment variables.
  This is specific to Node.js but that's the only thing I've tested.  Setting this will
  enable coverage information in the __browser__, which is important for coverage analysis
  during testing.  You also need to set this in the back APIs as well, but the `package.json`
  has setups for this too.

  + `url string` -- The starting URL for the Playwright instance.
  + `{browser, context, p: page}` -- Almost everything you need right away.
 */
export const playstart = async (url) => {
  if(!HEADLESS) console.log("Running playwright in visible mode (not HEADLESS).");
  if(COVERAGE) console.log(`Coverage reports going to ${COVERAGE}.`);

  const browser = await playwright['chromium'].launch({headless: HEADLESS});
  const context = await browser.newContext();
  const p = await context.newPage();

  if(COVERAGE) {
    await p.coverage.startJSCoverage();
  }

  p.on('console', async msg =>  {
    console.debug(`CONSOLE ${msg.type()}>>`, msg)
  });

  await p.goto(url);
  return {browser, context, p};
}

/*
  Properly stops [Playwright](https://playwright.dev) if you have coverage enabled (see `playstop`).
  There's a lot of things you need to do when you stop coverage, so use this or your
  coverage will not actually be saved.  The file is saved in the directory specified by
  `NODE_V8_COVERAGE`, which is usually `.coverage`.

  + `browser Object` -- Playwright browser object from `playstart`.
  + `p Object` -- Playwright page object from `playstart`.
 */
export const playstop = async (browser, p) => {
  if(COVERAGE) {
    const coverage = await p.coverage.stopJSCoverage();
    for(const entry of coverage) {
      entry.url = path.join(process.cwd(), "public", url.parse(entry.url).pathname);
    }
    const v8out = {result: coverage, timestamp: Date.now()};
    fs.writeFileSync(`${COVERAGE}/${uuid()}.json`, JSON.stringify(v8out));
  }

  await browser.close();
}

/*
  Waits for the `css_id` selector to be visible on the page.  Most external
  browser controller systems are incredibly annoying because they're based on
  timing and aysnc waiting for things in the page.  This does all the annoying
  waiting and logging an error message so you can figure out what's going on.

  + `p Object` -- Playwright page object.
  + `css_id string` -- CSS ID to wait for, use `tid()` to use a `data-testid`.
 */
export const wait = async (p, css_id) => {
  try {
    await p.waitForSelector(css_id, { timeout: WAIT_TIME });
  } catch(error) {
    console.error(error, `!!!!!!!!!!!!!!!!!!!!!!!!!!!! Failed waiting for id ${css_id}`);
    throw error;
  }
}

/*
  Uses the [Ava](https://github.com/avajs/ava) test variable `t` to confirm
  that a tag with the given `css_id` exists.  It will wait for this `css_id`,
  get the tag for it, then return the `tag.textContent()`. You can then do
  additional tests to make sure it contains the right content.

  + `t Object` -- Ava test object.
  + `p Object` -- Playwright page object.
  + `css_id string` -- Use a `tid()` to target a `data-testid`.
 */
export const expect = async (t, p, css_id) => {
  await wait(p, css_id);

  let tag = await p.$(css_id);
  let text = tag ? await tag.textContent() : "NULL!";
  t.true(text !== null);
  return text;
}

/*
  Due to the timing of requests from the browser to the backend
  you sometimes have to wait for data to get stored and things to
  happen.  This will do that by returning a `Promise` that resolves
  after a certain `ms` miliseconds.

  + `ms number` -- Miliseconds to wait, passed to `setTimeout`.
  + ___return___ `Promise` -- await on this.
 */
export const sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/*
  Generate a random user for use in login forms and other things.

  + return {email, full_name, initials, password}
 */
export const random_user = () => {
  let email = faker.internet.email();
  let rnd = random.int(0, 10 * 1000);
  email = email.replace('@', `${rnd}@`);
  let full_name = faker.name.findName();

  let initials = full_name.split(' ').map(x => x[0]).join('');
  let password = faker.internet.password();
  assert(password, "Looks like faker is failing at passwords again.");

  return {
    email,
    full_name,
    initials,
    password
  }
}

/*
   Generates a `random_user` and then register it in the database.
   It will add the field `user.raw_password` to the user that's returned
   so you can use the password in testing.

   + `is_admin boolean (false)` -- Should this user be an admin or not?
   + ___return___ `User` -- The `User` object from `lib/models.js`.
 */
export const register_user = async (is_admin=false) => {
  const rando = random_user();
  rando.password_repeat = rando.password;
  // copy it to keep the original password
  const user = await User.register({...rando});

  if(is_admin) {
    // make this user an admin
    user.admin = true;
    await User.update({id: user.id}, {admin: true});
  }
  // then replace it so we can use it later
  user.raw_password = rando.password;
  return user;
}

/*
  A helper function for other tests to use logins.

  ___FOOTGUN___: It must have `email` and `raw_password` in it for this to work.  The `register_user` function sets this up for you but if you want to query or craft your own `User` you'll need to ensure that.

  + `t Object` -- Ava testing object.
  + `p Object` -- Playwright page object.
  + `with_user User` -- A `User` object from `lib/models.js`.
 */
export const login = async (t, p, with_user) => {
  const user = with_user ? with_user : await register_user();

  await expect(t, p, tid("login-form"));
  // fill out correct form, and redirect to watch
  await p.fill('#email', user.email);
  await p.fill("#password", user.raw_password);
  await p.click(tid('login-button'));
  await sleep(1000);

  return user;
}
