/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const RuleRewriter = require("resource://devtools/client/fronts/inspector/rule-rewriter.js");
const {
  isCssPropertyKnown,
} = require("resource://devtools/server/actors/css-properties.js");

const TEST_DATA = [
  {
    desc: "simple set",
    input: "p:v;",
    instruction: { type: "set", name: "p", value: "N", priority: "", index: 0 },
    expected: "p:N;",
  },
  {
    desc: "set in rule with nested rule",
    input: "a:b; span { e:f; }",
    instruction: { type: "set", name: "a", value: "N", priority: "", index: 0 },
    expected: "a:N; span { e:f; }",
  },
  {
    desc: "simple set clearing !important",
    input: "p:v !important;",
    instruction: { type: "set", name: "p", value: "N", priority: "", index: 0 },
    expected: "p:N;",
  },
  {
    desc: "simple set adding !important",
    input: "p:v;",
    instruction: {
      type: "set",
      name: "p",
      value: "N",
      priority: "important",
      index: 0,
    },
    expected: "p:N !important;",
  },
  {
    desc: "simple set between comments",
    input: "/*color:red;*/ p:v; /*color:green;*/",
    instruction: { type: "set", name: "p", value: "N", priority: "", index: 1 },
    expected: "/*color:red;*/ p:N; /*color:green;*/",
  },
  // The rule view can generate a "set" with a previously unknown
  // property index; which should work like "create".
  {
    desc: "set at unknown index",
    input: "a:b; e: f;",
    instruction: { type: "set", name: "c", value: "d", priority: "", index: 2 },
    expected: "a:b; e: f;c: d;",
  },
  {
    desc: "simple rename",
    input: "p:v;",
    instruction: { type: "rename", name: "p", newName: "q", index: 0 },
    expected: "q:v;",
  },
  {
    desc: "rename in rule with nested rule",
    input: "a:b; span { e:f; }",
    instruction: { type: "rename", name: "a", newName: "q", index: 0 },
    expected: "q:b; span { e:f; }",
  },
  // "rename" is passed the name that the user entered, and must do
  // any escaping necessary to ensure that this is an identifier.
  {
    desc: "rename requiring escape",
    input: "p:v;",
    instruction: { type: "rename", name: "p", newName: "a b", index: 0 },
    expected: "a\\ b:v;",
  },
  {
    desc: "simple create",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "v",
      priority: "important",
      index: 0,
      enabled: true,
    },
    expected: "p: v !important;",
  },
  {
    desc: "create between two properties",
    input: "a:b; e: f;",
    instruction: {
      type: "create",
      name: "c",
      value: "d",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "a:b; c: d;e: f;",
  },
  {
    desc: "create at the end of rule with nested rule",
    input: "a:b; span { e:f; }",
    instruction: {
      type: "create",
      name: "c",
      value: "d",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "a:b; c: d;span { e:f; }",
  },
  {
    desc: "create at the end of rule with nested rule and line breaks",
    input: `
      position: absolute;
      &.class {}`,
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: `
      position: absolute;
      color: red;
      &.class {}`,
  },
  {
    desc: "create at the end of rule with CSSNestedDeclarations",
    input: "a:b; span { e:f; } g: h;",
    instruction: {
      type: "create",
      name: "c",
      value: "d",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "a:b; c: d;span { e:f; } g: h;",
  },
  // "create" is passed the name that the user entered, and must do
  // any escaping necessary to ensure that this is an identifier.
  {
    desc: "create requiring escape",
    input: "",
    instruction: {
      type: "create",
      name: "a b",
      value: "d",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "a\\ b: d;",
  },
  {
    desc: "simple disable",
    input: "p:v;",
    instruction: { type: "enable", name: "p", value: false, index: 0 },
    expected: "/*! p:v; */",
  },
  {
    desc: "simple enable",
    input: "/* color:v; */",
    instruction: { type: "enable", name: "color", value: true, index: 0 },
    expected: "color:v;",
  },
  {
    desc: "enable with following property in comment",
    input: "/* color:red; color: blue; */",
    instruction: { type: "enable", name: "color", value: true, index: 0 },
    expected: "color:red; /* color: blue; */",
  },
  {
    desc: "enable with preceding property in comment",
    input: "/* color:red; color: blue; */",
    instruction: { type: "enable", name: "color", value: true, index: 1 },
    expected: "/* color:red; */ color: blue;",
  },
  {
    desc: "disable on rule with nested rule",
    input: "a:b; span { c:d; }",
    instruction: { type: "enable", name: "a", value: false, index: 0 },
    expected: "/*! a:b; */ span { c:d; }",
  },
  {
    desc: "enable on rule with nested rule",
    input: "/*! a:b; */ span { c:d; }",
    instruction: { type: "enable", name: "a", value: true, index: 0 },
    expected: "a:b; span { c:d; }",
  },
  {
    desc: "simple remove",
    input: "a:b;c:d;e:f;",
    instruction: { type: "remove", name: "c", index: 1 },
    expected: "a:b;e:f;",
  },
  {
    desc: "remove on rule with nested rule",
    input: "a:b;c:d;span { e:f; }",
    instruction: { type: "remove", name: "c", index: 1 },
    expected: "a:b;span { e:f; }",
  },
  {
    desc: "disable with comment ender in string",
    input: "content: '*/';",
    instruction: { type: "enable", name: "content", value: false, index: 0 },
    expected: "/*! content: '*\\/'; */",
  },
  {
    desc: "enable with comment ender in string",
    input: "/* content: '*\\/'; */",
    instruction: { type: "enable", name: "content", value: true, index: 0 },
    expected: "content: '*/';",
  },
  {
    desc: "enable requiring semicolon insertion",
    // Note the lack of a trailing semicolon in the comment.
    input: "/* color:red */ color: blue;",
    instruction: { type: "enable", name: "color", value: true, index: 0 },
    expected: "color:red; color: blue;",
  },
  {
    desc: "create requiring semicolon insertion",
    // Note the lack of a trailing semicolon.
    input: "color: red",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "color: red;a: b;",
  },

  // Newline insertion.
  {
    desc: "simple newline insertion",
    input: "\ncolor: red;\n",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\ncolor: red;\na: b;\n",
  },
  // Newline insertion.
  {
    desc: "semicolon insertion before newline",
    // Note the lack of a trailing semicolon.
    input: "\ncolor: red\n",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\ncolor: red;\na: b;\n",
  },
  // Newline insertion.
  {
    desc: "newline and semicolon insertion",
    // Note the lack of a trailing semicolon and newline.
    input: "\ncolor: red",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\ncolor: red;\na: b;\n",
  },

  // Newline insertion and indentation.
  {
    desc: "indentation with create",
    input: "\n  color: red;\n",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\n  color: red;\n  a: b;\n",
  },
  // Newline insertion and indentation.
  {
    desc: "indentation plus semicolon insertion before newline",
    // Note the lack of a trailing semicolon.
    input: "\n  color: red\n",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\n  color: red;\n  a: b;\n",
  },
  {
    desc: "indentation inserted before trailing whitespace",
    // Note the trailing whitespace.  This could come from a rule
    // like:
    // @supports (mumble) {
    //   body {
    //     color: red;
    //   }
    // }
    // Here if we create a rule we don't want it to follow
    // the indentation of the "}".
    input: "\n    color: red;\n  ",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\n    color: red;\n    a: b;\n  ",
  },
  // Newline insertion and indentation.
  {
    desc: "indentation comes from preceding comment",
    // Note how the comment comes before the declaration.
    input: "\n  /* comment */ color: red\n",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "\n  /* comment */ color: red;\n  a: b;\n",
  },
  // Default indentation.
  {
    desc: "use of default indentation",
    input: "\n",
    instruction: {
      type: "create",
      name: "a",
      value: "b",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "\n\ta: b;\n",
  },

  // Deletion handles newlines properly.
  {
    desc: "deletion removes newline",
    input: "a:b;\nc:d;\ne:f;",
    instruction: { type: "remove", name: "c", index: 1 },
    expected: "a:b;\ne:f;",
  },
  // Deletion handles newlines properly.
  {
    desc: "deletion remove blank line",
    input: "\n  a:b;\n  c:d;  \ne:f;",
    instruction: { type: "remove", name: "c", index: 1 },
    expected: "\n  a:b;\ne:f;",
  },
  // Deletion handles newlines properly.
  {
    desc: "deletion leaves comment",
    input: "\n  a:b;\n  /* something */ c:d;  \ne:f;",
    instruction: { type: "remove", name: "c", index: 1 },
    expected: "\n  a:b;\n  /* something */   \ne:f;",
  },
  // Deletion handles newlines properly.
  {
    desc: "deletion leaves previous newline",
    input: "\n  a:b;\n  c:d;  \ne:f;",
    instruction: { type: "remove", name: "e", index: 2 },
    expected: "\n  a:b;\n  c:d;  \n",
  },
  // Deletion handles newlines properly.
  {
    desc: "deletion removes trailing whitespace",
    input: "\n  a:b;\n  c:d;  \n    e:f;",
    instruction: { type: "remove", name: "e", index: 2 },
    expected: "\n  a:b;\n  c:d;  \n",
  },
  // Deletion handles newlines properly.
  {
    desc: "deletion preserves indentation",
    input: "  a:b;\n  c:d;  \n    e:f;",
    instruction: { type: "remove", name: "a", index: 0 },
    expected: "  c:d;  \n    e:f;",
  },

  // Termination insertion corner case.
  {
    desc: "enable single quote termination",
    input: "/* content: 'hi */ color: red;",
    instruction: { type: "enable", name: "content", value: true, index: 0 },
    expected: "content: 'hi'; color: red;",
    changed: { 0: "'hi'" },
  },
  // Termination insertion corner case.
  {
    desc: "create single quote termination",
    input: "content: 'hi",
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "content: 'hi';color: red;",
    changed: { 0: "'hi'" },
  },

  // Termination insertion corner case.
  {
    desc: "enable double quote termination",
    input: '/* content: "hi */ color: red;',
    instruction: { type: "enable", name: "content", value: true, index: 0 },
    expected: 'content: "hi"; color: red;',
    changed: { 0: '"hi"' },
  },
  // Termination insertion corner case.
  {
    desc: "create double quote termination",
    input: 'content: "hi',
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: 'content: "hi";color: red;',
    changed: { 0: '"hi"' },
  },

  // Termination insertion corner case.
  {
    desc: "enable url termination",
    input: "/* background-image: url(something.jpg */ color: red;",
    instruction: {
      type: "enable",
      name: "background-image",
      value: true,
      index: 0,
    },
    expected: "background-image: url(something.jpg); color: red;",
    changed: { 0: "url(something.jpg)" },
  },
  // Termination insertion corner case.
  {
    desc: "create url termination",
    input: "background-image: url(something.jpg",
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "background-image: url(something.jpg);color: red;",
    changed: { 0: "url(something.jpg)" },
  },

  // Termination insertion corner case.
  {
    desc: "enable url single quote termination",
    input: "/* background-image: url('something.jpg */ color: red;",
    instruction: {
      type: "enable",
      name: "background-image",
      value: true,
      index: 0,
    },
    expected: "background-image: url('something.jpg'); color: red;",
    changed: { 0: "url('something.jpg')" },
  },
  // Termination insertion corner case.
  {
    desc: "create url single quote termination",
    input: "background-image: url('something.jpg",
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "background-image: url('something.jpg');color: red;",
    changed: { 0: "url('something.jpg')" },
  },

  // Termination insertion corner case.
  {
    desc: "create url double quote termination",
    input: '/* background-image: url("something.jpg */ color: red;',
    instruction: {
      type: "enable",
      name: "background-image",
      value: true,
      index: 0,
    },
    expected: 'background-image: url("something.jpg"); color: red;',
    changed: { 0: 'url("something.jpg")' },
  },
  // Termination insertion corner case.
  {
    desc: "enable url double quote termination",
    input: 'background-image: url("something.jpg',
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: 'background-image: url("something.jpg");color: red;',
    changed: { 0: 'url("something.jpg")' },
  },

  // Termination insertion corner case.
  {
    desc: "create backslash termination",
    input: "something: \\",
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "something: \\\\;color: red;",
    changed: { 0: "\\\\" },
  },

  // Termination insertion corner case.
  {
    desc: "enable backslash single quote termination",
    input: "something: '\\",
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "something: '\\\\';color: red;",
    changed: { 0: "'\\\\'" },
  },
  {
    desc: "enable backslash double quote termination",
    input: 'something: "\\',
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: 'something: "\\\\";color: red;',
    changed: { 0: '"\\\\"' },
  },

  // Termination insertion corner case.
  {
    desc: "enable comment termination",
    input: "something: blah /* comment ",
    instruction: {
      type: "create",
      name: "color",
      value: "red",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "something: blah /* comment*/; color: red;",
  },

  // Rewrite a "heuristic override" comment.
  {
    desc: "enable with heuristic override comment",
    input: "/*! walrus: zebra; */",
    instruction: { type: "enable", name: "walrus", value: true, index: 0 },
    expected: "walrus: zebra;",
  },

  // Sanitize a bad value.
  {
    desc: "create sanitize unpaired brace",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "}",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: \\};",
    changed: { 0: "\\}" },
  },
  // Sanitize a bad value.
  {
    desc: "set sanitize unpaired brace",
    input: "walrus: zebra;",
    instruction: {
      type: "set",
      name: "walrus",
      value: "{{}}}",
      priority: "",
      index: 0,
    },
    expected: "walrus: {{}}\\};",
    changed: { 0: "{{}}\\}" },
  },
  // Sanitize a bad value.
  {
    desc: "enable sanitize unpaired brace",
    input: "/*! walrus: }*/",
    instruction: { type: "enable", name: "walrus", value: true, index: 0 },
    expected: "walrus: \\};",
    changed: { 0: "\\}" },
  },

  // Creating a new declaration does not require an attempt to
  // terminate a previous commented declaration.
  {
    desc: "disabled declaration does not need semicolon insertion",
    input: "/*! no: semicolon */\n",
    instruction: {
      type: "create",
      name: "walrus",
      value: "zebra",
      priority: "",
      index: 1,
      enabled: true,
    },
    expected: "/*! no: semicolon */\nwalrus: zebra;\n",
    changed: {},
  },

  {
    desc: "create commented-out property",
    input: "p: v",
    instruction: {
      type: "create",
      name: "shoveler",
      value: "duck",
      priority: "",
      index: 1,
      enabled: false,
    },
    expected: "p: v;/*! shoveler: duck; */",
  },
  {
    desc: "disabled create with comment ender in string",
    input: "",
    instruction: {
      type: "create",
      name: "content",
      value: "'*/'",
      priority: "",
      index: 0,
      enabled: false,
    },
    expected: "/*! content: '*\\/'; */",
  },

  {
    desc: "delete disabled property",
    input: "\n  a:b;\n  /* color:#f06; */\n  e:f;",
    instruction: { type: "remove", name: "color", index: 1 },
    expected: "\n  a:b;\n  e:f;",
  },
  {
    desc: "delete heuristic-disabled property",
    input: "\n  a:b;\n  /*! c:d; */\n  e:f;",
    instruction: { type: "remove", name: "c", index: 1 },
    expected: "\n  a:b;\n  e:f;",
  },
  {
    desc: "delete disabled property leaving other disabled property",
    input: "\n  a:b;\n  /* color:#f06; background-color: seagreen; */\n  e:f;",
    instruction: { type: "remove", name: "color", index: 1 },
    expected: "\n  a:b;\n   /* background-color: seagreen; */\n  e:f;",
  },

  {
    desc: "regression test for bug 1328016",
    input: "position:absolute;top50px;height:50px;width:50px;",
    instruction: {
      type: "set",
      name: "width",
      value: "60px",
      priority: "",
      index: 2,
    },
    expected: "position:absolute;top50px;height:50px;width:60px;",
  },

  {
    desc: "url regression test for bug 1321970",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "url(",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: url();",
    changed: { 0: "url()" },
  },

  {
    desc: "url semicolon regression test for bug 1321970",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "url(;",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: url();",
    changed: { 0: "url()" },
  },

  {
    desc: "basic regression test for bug 1321970",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "(",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: \\(;",
    changed: { 0: "\\(" },
  },

  {
    desc: "unbalanced regression test for bug 1321970",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "({[})",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: ({\\[});",
    changed: { 0: "({\\[})" },
  },

  {
    desc: "function regression test for bug 1321970",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "func(1,2)",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: func(1,2);",
  },

  {
    desc: "function regression test for bug 1355233",
    input: "",
    instruction: {
      type: "create",
      name: "p",
      value: "func(",
      priority: "",
      index: 0,
      enabled: true,
    },
    expected: "p: func\\(;",
    changed: { 0: "func\\(" },
  },
];

function rewriteDeclarations(
  win,
  inputString,
  instruction,
  defaultIndentation
) {
  const rewriter = new RuleRewriter(win, isCssPropertyKnown, null, inputString);
  rewriter.defaultIndentation = defaultIndentation;

  switch (instruction.type) {
    case "rename":
      rewriter.renameProperty(
        instruction.index,
        instruction.name,
        instruction.newName
      );
      break;

    case "enable":
      rewriter.setPropertyEnabled(
        instruction.index,
        instruction.name,
        instruction.value
      );
      break;

    case "create":
      rewriter.createProperty(
        instruction.index,
        instruction.name,
        instruction.value,
        instruction.priority,
        instruction.enabled
      );
      break;

    case "set":
      rewriter.setProperty(
        instruction.index,
        instruction.name,
        instruction.value,
        instruction.priority
      );
      break;

    case "remove":
      rewriter.removeProperty(instruction.index, instruction.name);
      break;

    default:
      throw new Error("unrecognized instruction");
  }

  return rewriter.getResult();
}

function run_test() {
  const win =
    Services.appShell.createWindowlessBrowser(false).document.defaultView;

  for (const test of TEST_DATA) {
    const { changed, text } = rewriteDeclarations(
      win,
      test.input,
      test.instruction,
      "\t"
    );
    equal(text, test.expected, "output for " + test.desc);

    let expectChanged;
    if ("changed" in test) {
      expectChanged = test.changed;
    } else {
      expectChanged = {};
    }
    deepEqual(changed, expectChanged, "changed result for " + test.desc);
  }
}
