import fs from "fs";

import {
  getTimeObj,
  getTimeStr,
  clamp,
  parseViscolors,
  parseIni,
  normalize,
  denormalize,
  segment,
  moveSelected,
  spliceIn,
  getFileExtension,
  makeCachingFilterFunction
} from "./utils";

const fixture = (filename: string) =>
  fs.readFileSync(`./js/__tests__/fixtures/${filename}`, "utf8");

describe("getTimeObj", () => {
  it("expresses seconds as an object", () => {
    const actual = getTimeObj(1234);
    const expected = {
      minutesFirstDigit: "2",
      minutesSecondDigit: "0",
      secondsFirstDigit: "3",
      secondsSecondDigit: "4"
    };
    expect(actual).toEqual(expected);
  });
});

describe("getTimeStr", () => {
  it("expresses seconds as string", () => {
    const actual = getTimeStr(1234);
    const expected = "20:34";
    expect(actual).toEqual(expected);
  });
  it("pads with only one zero", () => {
    const actual = getTimeStr(5);
    const expected = "0:05";
    expect(actual).toEqual(expected);
  });
  it("truncates extra minutes", () => {
    const actual = getTimeStr(540000);
    const expected = "9000:00";
    expect(actual).toEqual(expected);
  });
});

describe("getFileExtension", () => {
  it("can get bmp", () => {
    expect(getFileExtension("foo.bmp")).toBe("bmp");
  });
  it("can match four char extension", () => {
    expect(getFileExtension("foo.html")).toBe("html");
  });
  it("converts to lower case", () => {
    expect(getFileExtension("foo.BMP")).toBe("bmp");
  });
  it("returns null if a match is not found", () => {
    expect(getFileExtension("foo")).toBe(null);
  });
});

describe("clamp", () => {
  it("respects the max value", () => {
    const actual = clamp(101, 0, 100);
    const expected = 100;
    expect(actual).toEqual(expected);
  });
  it("respects the min value", () => {
    const actual = clamp(0, 1, 100);
    const expected = 1;
    expect(actual).toEqual(expected);
  });
  it("respects the given value if in range", () => {
    const actual = clamp(50, 0, 100);
    const expected = 50;
    expect(actual).toEqual(expected);
  });
});

describe("parseViscolors", () => {
  it("can parse the default viscolors file", () => {
    const viscolors = fixture("VISCOLOR.TXT");
    const actual = parseViscolors(viscolors);
    const expected = [
      "rgb(0,0,0)",
      "rgb(24,33,41)",
      "rgb(239,49,16)",
      "rgb(206,41,16)",
      "rgb(214,90,0)",
      "rgb(214,102,0)",
      "rgb(214,115,0)",
      "rgb(198,123,8)",
      "rgb(222,165,24)",
      "rgb(214,181,33)",
      "rgb(189,222,41)",
      "rgb(148,222,33)",
      "rgb(41,206,16)",
      "rgb(50,190,16)",
      "rgb(57,181,16)",
      "rgb(49,156,8)",
      "rgb(41,148,0)",
      "rgb(24,132,8)",
      "rgb(255,255,255)",
      "rgb(214,214,222)",
      "rgb(181,189,189)",
      "rgb(160,170,175)",
      "rgb(148,156,165)",
      "rgb(150,150,150)"
    ];
    expect(actual).toEqual(expected);
  });

  it("can parse a malformed viscolors file", () => {
    // From https://skins.webamp.org/skin/018ddb394f2bfe49efa70bce27b71cb2/Centra_CSS-102_104-3.wsz/
    const viscolors = fixture("CENTRA_VISCOLOR.TXT");
    const actual = parseViscolors(viscolors);
    const expected = [
      "rgb(110,150,176)", // 0
      "rgb(165,165,165)", // 1
      "rgb(55,55,67)", // 2
      "rgb(55,55,67)", // 3
      "rgb(55,55,67)", // 4
      "rgb(55,55,67)", // 5
      "rgb(55,55,67)", // 6
      "rgb(55,55,67)", // 7
      "rgb(55,55,67)", // 8
      "rgb(55,55,67)", // 9
      "rgb(55,55,67)", // 10
      "rgb(55,55,67)", // 11
      "rgb(55,55,67)", // 12
      "rgb(55,55,67)", // 13
      "rgb(55,55,67)", // 14
      "rgb(55,55,67)", // 15
      "rgb(55,55,67)", // 16
      "rgb(55,55,67)", // 17
      "rgb(55,55,67)", // 18
      "rgb(55,55,67)", // 19
      "rgb(55,55,67)", // 20
      "rgb(55,55,67)", // 21
      "rgb(55,55,67)", // 22
      //c 2
      "rgb(181,189,189)", // 20 = osc 3
      "rgb(148,156,165)", // 21 = osc 4
      "rgb(148,156,165)" // 2) = osc 5 (dimmest)
    ];
    expect(actual).toEqual(expected);
  });

  it("does not require commas to separate values", () => {
    // From https://skins.webamp.org/skin/99c6227d8880e00813a9aa6c4e808c37/valgaav_by_dreamcass-d85bqwp.wsz/
    const viscolors = fixture("viscolor_valgaav.txt");
    const actual = parseViscolors(viscolors);
    const expected = [
      "rgb(98,111,123)", // color 0 = background
      "rgb(98,111,123)", // color 1 = dots
      "rgb(21,21,21)", // 2 = top of spec
      "rgb(223,176,176)", // 16
      "rgb(218,168,168)", // 15
      "rgb(211,158,158)", // 14
      "rgb(204,147,147)", // 13
      "rgb(198,137,137)", // 12
      "rgb(191,127,127)", // 11
      "rgb(185,117,117)", // 10
      "rgb(178,107,107)", // 9
      "rgb(172,97,97)", // 8
      "rgb(165,86,86)", // 7
      "rgb(158,76,76)", // 6
      "rgb(152,66,66)", // 5
      "rgb(145,56,56)", // 4
      "rgb(138,46,46)", // 3
      "rgb(138,46,46)", // 17 = bottom of spec
      "rgb(138,46,46)", // 18 = osc 1
      "rgb(158,76,76)", // 19 = osc 2
      "rgb(178,107,107)", // 20 = osc 3
      "rgb(198,137,137)", // 21 = osc4
      "rgb(218,168,168)", // 22 = osc5
      "rgb(223,176,176)" // 23 = analyzer peak dots
    ];
    expect(actual).toEqual(expected);
  });
});

describe("parseIni", () => {
  it("can parse the default pledit.txt file", () => {
    const pledit = fixture("PLEDIT.TXT");
    const actual = parseIni(pledit);
    const expected = {
      text: {
        normal: "#00FF00",
        current: "#FFFFFF",
        normalbg: "#000000",
        selectedbg: "#0000FF",
        font: "Arial"
      }
    };
    expect(actual).toEqual(expected);
  });

  it("can parse TopazAmp's pledit.txt file", () => {
    const pledit = fixture("PLEDIT_TOPAZ.TXT");
    const actual = parseIni(pledit);
    const expected = {
      text: {
        normal: "#319593",
        current: "#89D8D1",
        normalbg: "#000000",
        selectedbg: "#2B4242",
        font: "Arial",
        mbbg: "#000000",
        mbfg: "#89D8D1"
      }
    };
    expect(actual).toEqual(expected);
  });

  it("allows space around =", () => {
    const actual = parseIni(`
[foo]
bar = baz
`);
    const expected = {
      foo: {
        bar: "baz"
      }
    };
    expect(actual).toEqual(expected);
  });

  it("can parse a pledit.txt file with quotes", () => {
    const pledit = fixture("PLEDIT_WITH_QUOTES.TXT");
    const actual = parseIni(pledit);
    const expected = {
      text: {
        normal: "#00FF00",
        current: "#FFFFFF",
        normalbg: "#000000",
        selectedbg: "#0000FF",
        font: "Ricky's cool font!"
      }
    };
    expect(actual).toEqual(expected);
  });

  it("allows quotes around values", () => {
    const actual = parseIni(`
[foo]
bar = "baz"
  `);
    const expected = {
      foo: {
        bar: "baz"
      }
    };
    expect(actual).toEqual(expected);
  });
});

test("normalize", () => {
  expect(normalize(1)).toBe(1);
  expect(normalize(64)).toBe(100);
});
test("denormalize", () => {
  expect(denormalize(1)).toBe(1);
  expect(denormalize(100)).toBe(64);
});

describe("segment", () => {
  it("can handle min", () => {
    expect(segment(0, 100, 0, [0, 1, 2])).toBe(0);
    expect(segment(1, 100, 1, [0, 1, 2])).toBe(0);
    expect(segment(-1, 100, -1, [0, 1, 2])).toBe(0);
  });
  it("can handle max", () => {
    //expect(segment(0, 100, 100, [0, 1, 2])).toBe(2);
    //expect(segment(1, 100, 100, [0, 1, 2])).toBe(2);
    expect(segment(-1, 100, 100, [0, 1, 2])).toBe(2);
  });
  it("can handle mid", () => {
    expect(segment(0, 2, 1, [0, 1, 2])).toBe(1);
    expect(segment(0, 2, 1.5, [0, 1, 2])).toBe(2);
    expect(segment(1, 3, 2.5, [0, 1, 2])).toBe(2);
    expect(segment(-1, 2, 0.5, [0, 1, 2])).toBe(1);
  });
  it("can handle various real wold cases", () => {
    expect(segment(-100, 100, -100, ["left", "center", "right"])).toBe("left");
    expect(segment(0, 100, 88, ["left", "center", "right"])).toBe("right");
    expect(segment(0, 100, 50, ["left", "center", "right"])).toBe("center");
  });
});

describe("moveSelected", () => {
  it("can drag a single item 1", () => {
    expect(
      moveSelected(
        ["a", "b", "c", "d", "e", "f", "g", "h"],
        i => new Set([1]).has(i),
        1
      )
    ).toEqual(["a", "c", "b", "d", "e", "f", "g", "h"]);
  });
  it("can drag a single item", () => {
    expect(
      moveSelected(
        ["a", "b", "c", "d", "e", "f", "g", "h"],
        i => new Set([1]).has(i),
        3
      )
    ).toEqual(["a", "c", "d", "e", "b", "f", "g", "h"]);
  });
  it("can drag consecutive items", () => {
    expect(
      moveSelected(
        ["a", "b", "c", "d", "e", "f", "g", "h"],
        i => new Set([1, 2]).has(i),
        3
      )
    ).toEqual(["a", "d", "e", "f", "b", "c", "g", "h"]);
  });
  it("works for a simple example", () => {
    const arr = [true, false, false];
    expect(moveSelected(arr, i => arr[i], 1)).toEqual([false, true, false]);
  });
  it("works for a simple negative example", () => {
    const arr = [false, false, true];
    expect(moveSelected(arr, i => arr[i], -1)).toEqual([false, true, false]);
  });
});

describe("spliceIn", () => {
  it("is immutable", () => {
    const original = [1, 2, 3];
    const spliced = spliceIn(original, 1, [200]);
    expect(spliced).not.toBe(original);
    expect(original).toEqual([1, 2, 3]);
  });
  it("adds values at the given index", () => {
    const spliced = spliceIn([1, 2, 3], 1, [200]);
    expect(spliced).toEqual([1, 200, 2, 3]);
  });
});

describe("makeCachingFilterFunction", () => {
  test("caches exact queries", () => {
    const values = ["abc", "b", "c"];
    const includes = jest.fn((v, query) => v.includes(query));
    const filter = makeCachingFilterFunction(values, includes);
    expect(filter("c")).toEqual(["abc", "c"]);
    expect(includes.mock.calls.length).toBe(3);
    expect(filter("c")).toEqual(["abc", "c"]);
    expect(includes.mock.calls.length).toBe(3);
  });

  test("caches sub queries", () => {
    const values = ["a--", "ab-", "abc"];
    const includes = jest.fn((v, query) => v.includes(query));
    let comparisons = 0;
    const newComparisons = () => {
      const recent = includes.mock.calls.length - comparisons;
      comparisons += recent;
      return recent;
    };
    const filter = makeCachingFilterFunction(values, includes);
    // Intial search
    expect(filter("ab")).toEqual(["ab-", "abc"]);
    expect(newComparisons()).toBe(3); // Looks at all elements

    // Second search where original search is a prefix
    expect(filter("abc")).toEqual(["abc"]);
    expect(newComparisons()).toBe(2); // Only reconsiders the previous matches

    // Unique search
    expect(filter("b")).toEqual(["ab-", "abc"]); // Looks at all elements
    expect(newComparisons()).toBe(3); // Reconsiders all elements

    expect(filter("bc")).toEqual(["abc"]); // Only reconsidres the matches that already include `b`
    expect(newComparisons()).toBe(2);

    // Go back to the initial serach
    expect(filter("ab")).toEqual(["ab-", "abc"]);
    expect(newComparisons()).toBe(0); // Result is cached

    // A variation on the second search
    expect(filter("abcd")).toEqual([]);
    expect(newComparisons()).toBe(1); // Only recondsiders the results of `abc`
  });

  test("big data", () => {
    const values = [...Array(10000)].map((val, i) => String(i));
    const includes = jest.fn((v, query) => v.includes(query));
    let comparisons = 0;
    const newComparisons = () => {
      const recent = includes.mock.calls.length - comparisons;
      comparisons += recent;
      return recent;
    };
    const filter = makeCachingFilterFunction(values, includes);
    // Intial search
    expect(filter("").length).toEqual(10000);
    expect(newComparisons()).toBe(0); // Looks at zero

    expect(filter("1").length).toEqual(3439);
    expect(newComparisons()).toBe(10000); // Looks at all elements

    expect(filter("12").length).toEqual(299);
    expect(newComparisons()).toBe(3439);

    expect(filter("123").length).toEqual(20);
    expect(newComparisons()).toBe(299);

    expect(filter("1234").length).toEqual(1);
    expect(newComparisons()).toBe(20);

    expect(filter("12345").length).toEqual(0);
    expect(newComparisons()).toBe(1);

    // A variation on the initial non-empty query
    expect(filter("11").length).toEqual(280);
    expect(newComparisons()).toBe(3439);

    expect(filter("111").length).toEqual(19);
    expect(newComparisons()).toBe(280);

    expect(filter("1111").length).toEqual(1);
    expect(newComparisons()).toBe(19);
  });
});
