// Ported from Go
// https://github.com/golang/go/blob/go1.12.5/src/encoding/hex/hex.go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { assertEquals, assertThrows } from "../testing/asserts.ts";

import {
  encodedLen,
  encode,
  encodeToString,
  decodedLen,
  decode,
  decodeString,
  errLength,
  errInvalidByte,
} from "./hex.ts";

function toByte(s: string): number {
  return new TextEncoder().encode(s)[0];
}

const testCases = [
  // encoded(hex) / decoded(Uint8Array)
  ["", []],
  ["0001020304050607", [0, 1, 2, 3, 4, 5, 6, 7]],
  ["08090a0b0c0d0e0f", [8, 9, 10, 11, 12, 13, 14, 15]],
  ["f0f1f2f3f4f5f6f7", [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]],
  ["f8f9fafbfcfdfeff", [0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff]],
  ["67", Array.from(new TextEncoder().encode("g"))],
  ["e3a1", [0xe3, 0xa1]],
];

const errCases = [
  // encoded(hex) / error
  ["", "", undefined],
  ["0", "", errLength()],
  ["zd4aa", "", errInvalidByte(toByte("z"))],
  ["d4aaz", "\xd4\xaa", errInvalidByte(toByte("z"))],
  ["30313", "01", errLength()],
  ["0g", "", errInvalidByte(new TextEncoder().encode("g")[0])],
  ["00gg", "\x00", errInvalidByte(new TextEncoder().encode("g")[0])],
  ["0\x01", "", errInvalidByte(new TextEncoder().encode("\x01")[0])],
  ["ffeed", "\xff\xee", errLength()],
];

Deno.test({
  name: "[encoding.hex] encodedLen",
  fn(): void {
    assertEquals(encodedLen(0), 0);
    assertEquals(encodedLen(1), 2);
    assertEquals(encodedLen(2), 4);
    assertEquals(encodedLen(3), 6);
    assertEquals(encodedLen(4), 8);
  },
});

Deno.test({
  name: "[encoding.hex] encode",
  fn(): void {
    {
      const srcStr = "abc";
      const src = new TextEncoder().encode(srcStr);
      const dest = new Uint8Array(encodedLen(src.length));
      const int = encode(dest, src);
      assertEquals(src, new Uint8Array([97, 98, 99]));
      assertEquals(int, 6);
    }

    {
      const srcStr = "abc";
      const src = new TextEncoder().encode(srcStr);
      const dest = new Uint8Array(2); // out of index
      assertThrows(
        (): void => {
          encode(dest, src);
        },
        Error,
        "Out of index."
      );
    }

    for (const [enc, dec] of testCases) {
      const dest = new Uint8Array(encodedLen(dec.length));
      const src = new Uint8Array(dec as number[]);
      const n = encode(dest, src);
      assertEquals(dest.length, n);
      assertEquals(new TextDecoder().decode(dest), enc);
    }
  },
});

Deno.test({
  name: "[encoding.hex] encodeToString",
  fn(): void {
    for (const [enc, dec] of testCases) {
      assertEquals(encodeToString(new Uint8Array(dec as number[])), enc);
    }
  },
});

Deno.test({
  name: "[encoding.hex] decodedLen",
  fn(): void {
    assertEquals(decodedLen(0), 0);
    assertEquals(decodedLen(2), 1);
    assertEquals(decodedLen(4), 2);
    assertEquals(decodedLen(6), 3);
    assertEquals(decodedLen(8), 4);
  },
});

Deno.test({
  name: "[encoding.hex] decode",
  fn(): void {
    // Case for decoding uppercase hex characters, since
    // Encode always uses lowercase.
    const extraTestcase = [
      ["F8F9FAFBFCFDFEFF", [0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff]],
    ];

    const cases = testCases.concat(extraTestcase);

    for (const [enc, dec] of cases) {
      const dest = new Uint8Array(decodedLen(enc.length));
      const src = new TextEncoder().encode(enc as string);
      const [, err] = decode(dest, src);
      assertEquals(err, undefined);
      assertEquals(Array.from(dest), Array.from(dec as number[]));
    }
  },
});

Deno.test({
  name: "[encoding.hex] decodeString",
  fn(): void {
    for (const [enc, dec] of testCases) {
      const dst = decodeString(enc as string);

      assertEquals(dec, Array.from(dst));
    }
  },
});

Deno.test({
  name: "[encoding.hex] decode error",
  fn(): void {
    for (const [input, output, expectedErr] of errCases) {
      const out = new Uint8Array((input as string).length + 10);
      const [n, err] = decode(out, new TextEncoder().encode(input as string));
      assertEquals(
        new TextDecoder("ascii").decode(out.slice(0, n)),
        output as string
      );
      assertEquals(err, expectedErr);
    }
  },
});

Deno.test({
  name: "[encoding.hex] decodeString error",
  fn(): void {
    for (const [input, output, expectedErr] of errCases) {
      if (expectedErr) {
        assertThrows(
          (): void => {
            decodeString(input as string);
          },
          Error,
          (expectedErr as Error).message
        );
      } else {
        const out = decodeString(input as string);
        assertEquals(new TextDecoder("ascii").decode(out), output as string);
      }
    }
  },
});