"use strict"

const path = require("path")
const semver = require("semver")
const CLIEngine = require("eslint").CLIEngine
const plugin = require("..")

const eslintVersion = require("eslint/package.json").version

function isESLintVersion(descriptor) {
  return semver.satisfies(eslintVersion, descriptor)
}

function execute(file, baseConfig) {
  if (!baseConfig) baseConfig = {}

  const cli = new CLIEngine({
    extensions: ["html"],
    baseConfig: {
      settings: baseConfig.settings,
      rules: Object.assign(
        {
          "no-console": 2,
        },
        baseConfig.rules
      ),
    },
    ignore: false,
    useEslintrc: false,
    fix: baseConfig.fix,
  })
  cli.addPlugin("html", plugin)
  const results = cli.executeOnFiles([path.join(__dirname, "fixtures", file)])
    .results[0]
  return baseConfig.fix ? results : results && results.messages
}

it("should extract and remap messages", () => {
  const messages = execute("simple.html")

  expect(messages.length, 5)

  const hasEndPosition = messages[0].endLine !== undefined

  expect(messages[0].message).toBe("Unexpected console statement.")
  expect(messages[0].line).toBe(8)
  expect(messages[0].column).toBe(7)
  if (hasEndPosition) {
    expect(messages[0].endLine).toBe(8)
    expect(messages[0].endColumn).toBe(18)
  }

  expect(messages[1].message).toBe("Unexpected console statement.")
  expect(messages[1].line).toBe(14)
  expect(messages[1].column).toBe(7)
  if (hasEndPosition) {
    expect(messages[1].endLine).toBe(14)
    expect(messages[1].endColumn).toBe(18)
  }

  expect(messages[2].message).toBe("Unexpected console statement.")
  expect(messages[2].line).toBe(20)
  expect(messages[2].column).toBe(3)
  if (hasEndPosition) {
    expect(messages[2].endLine).toBe(20)
    expect(messages[2].endColumn).toBe(14)
  }

  expect(messages[3].message).toBe("Unexpected console statement.")
  expect(messages[3].line).toBe(25)
  expect(messages[3].column).toBe(11)
  if (hasEndPosition) {
    expect(messages[3].endLine).toBe(25)
    expect(messages[3].endColumn).toBe(22)
  }

  expect(messages[4].message).toBe("Unexpected console statement.")
  expect(messages[4].line).toBe(28)
  expect(messages[4].column).toBe(13)
  if (hasEndPosition) {
    expect(messages[4].endLine).toBe(28)
    expect(messages[4].endColumn).toBe(24)
  }
})

it("should report correct line numbers with crlf newlines", () => {
  const messages = execute("crlf-newlines.html")

  expect(messages.length).toBe(1)

  expect(messages[0].message).toBe("Unexpected console statement.")
  expect(messages[0].line).toBe(8)
  expect(messages[0].column).toBe(7)
})

describe("html/indent setting", () => {
  it("should automatically compute indent when nothing is specified", () => {
    const messages = execute("indent-setting.html", {
      rules: {
        indent: [2, 2],
      },
    })

    expect(messages.length).toBe(0)
  })

  it("should work with a zero absolute indentation descriptor", () => {
    const messages = execute("indent-setting.html", {
      rules: {
        indent: [2, 2],
      },

      settings: {
        "html/indent": 0,
      },
    })

    if (isESLintVersion(">= 4")) {
      expect(messages.length).toBe(9)

      // Only the first script is correctly indented (aligned on the first column)

      expect(messages[0].message).toMatch(
        /Expected indentation of 0 .* but found 2\./
      )
      expect(messages[0].line).toBe(16)

      expect(messages[1].message).toMatch(
        /Expected indentation of 2 .* but found 4\./
      )
      expect(messages[1].line).toBe(17)

      expect(messages[2].message).toMatch(
        /Expected indentation of 0 .* but found 2\./
      )
      expect(messages[2].line).toBe(18)

      expect(messages[3].message).toMatch(
        /Expected indentation of 0 .* but found 6\./
      )
      expect(messages[3].line).toBe(22)

      expect(messages[4].message).toMatch(
        /Expected indentation of 2 .* but found 8\./
      )
      expect(messages[4].line).toBe(23)

      expect(messages[5].message).toMatch(
        /Expected indentation of 0 .* but found 6\./
      )
      expect(messages[5].line).toBe(24)

      expect(messages[6].message).toMatch(
        /Expected indentation of 0 .* but found 10\./
      )
      expect(messages[6].line).toBe(28)

      expect(messages[7].message).toMatch(
        /Expected indentation of 2 .* but found 12\./
      )
      expect(messages[7].line).toBe(29)

      expect(messages[8].message).toMatch(
        /Expected indentation of 0 .* but found 10\./
      )
      expect(messages[8].line).toBe(30)
    }
    else {
      // ESlint < 4 indentation was always checked relatively to the previous line. Since every line
      // is correctly indented relatively to the previous one, there is no error.

      expect(messages.length).toBe(0)
    }
  })

  it("should work with a non-zero absolute indentation descriptor", () => {
    const messages = execute("indent-setting.html", {
      rules: {
        indent: [2, 2],
      },

      settings: {
        "html/indent": 2,
      },
    })

    if (isESLintVersion(">= 4")) {
      expect(messages.length).toBe(7)

      // The first script is incorrect since the second line gets dedented
      expect(messages[0].message).toMatch(
        /Expected indentation of 2 .* but found 0\./
      )
      expect(messages[0].line).toBe(11)

      // The second script is correct.

      expect(messages[1].message).toMatch(
        /Expected indentation of 0 .* but found 6\./
      )
      expect(messages[1].line).toBe(22)

      expect(messages[2].message).toMatch(
        /Expected indentation of .* but found 6\./
      )
      expect(messages[2].line).toBe(23)

      expect(messages[3].message).toMatch(
        /Expected indentation of .* but found 4\./
      )
      expect(messages[3].line).toBe(24)

      expect(messages[4].message).toMatch(
        /Expected indentation of 0 .* but found 10\./
      )
      expect(messages[4].line).toBe(28)

      expect(messages[5].message).toMatch(
        /Expected indentation of .* but found 10\./
      )
      expect(messages[5].line).toBe(29)

      expect(messages[6].message).toMatch(
        /Expected indentation of .* but found 8\./
      )
      expect(messages[6].line).toBe(30)
    }
    else {
      // ESlint < 4 indentation was always checked relatively to the previous line. Since some lines
      // are correctly indented relatively to the previous one, there is less errors.

      expect(messages.length).toBe(5)

      // The first script is incorrect since the second line gets dedented
      expect(messages[0].message).toMatch(
        /Expected indentation of 2 .* but found 0\./
      )
      expect(messages[0].line).toBe(11)

      // The second script is correct.

      expect(messages[1].message).toMatch(
        /Expected indentation of .* but found 6\./
      )
      expect(messages[1].line).toBe(23)

      expect(messages[2].message).toMatch(
        /Expected indentation of .* but found 4\./
      )
      expect(messages[2].line).toBe(24)

      expect(messages[3].message).toMatch(
        /Expected indentation of .* but found 10\./
      )
      expect(messages[3].line).toBe(29)

      expect(messages[4].message).toMatch(
        /Expected indentation of .* but found 8\./
      )
      expect(messages[4].line).toBe(30)
    }
  })

  it("should work with relative indentation descriptor", () => {
    const messages = execute("indent-setting.html", {
      rules: {
        indent: [2, 2],
      },

      settings: {
        "html/indent": "+2",
      },
    })

    if (isESLintVersion(">= 4")) {
      expect(messages.length).toBe(6)

      // The first script is correct since it can't be dedented, but follows the indent
      // rule anyway.

      expect(messages[0].message).toMatch(
        /Expected indentation of 0 .* but found 2\./
      )
      expect(messages[0].line).toBe(16)

      expect(messages[1].message).toMatch(
        /Expected indentation of 2 .* but found 4\./
      )
      expect(messages[1].line).toBe(17)

      expect(messages[2].message).toMatch(
        /Expected indentation of 0 .* but found 2\./
      )
      expect(messages[2].line).toBe(18)

      // The third script is correct.

      expect(messages[3].message).toMatch(
        /Expected indentation of 0 .* but found 10\./
      )
      expect(messages[3].line).toBe(28)

      expect(messages[4].message).toMatch(
        /Expected indentation of 2 .* but found 4\./
      )
      expect(messages[4].line).toBe(29)

      expect(messages[5].message).toMatch(
        /Expected indentation of 0 .* but found 2\./
      )
      expect(messages[5].line).toBe(30)
    }
    else {
      // ESlint < 4 indentation was always checked relatively to the previous line. Since some lines
      // are correctly indented relatively to the previous one, there is less errors.

      expect(messages.length).toBe(2)

      expect(messages[0].message).toMatch(
        /Expected indentation of 12 .* but found 4\./
      )
      expect(messages[0].line).toBe(29)

      expect(messages[1].message).toMatch(
        /Expected indentation of 10 .* but found 2\./
      )
      expect(messages[1].line).toBe(30)
    }
  })

  it("should report messages at the beginning of the file", () => {
    const messages = execute("error-at-the-beginning.html", {
      rules: {
        "max-lines": [2, { max: 1 }],
        "max-len": [2, { code: 35 }],
        "no-console": 0,
      },
    })

    expect(messages.length).toBe(2)
    expect(messages[0].message).toBe(
      "Line 1 exceeds the maximum line length of 35."
    )
    expect(messages[0].line).toBe(1)
    expect(messages[0].column).toBe(9)

    if (isESLintVersion(">= 3")) {
      expect(messages[1].message).toBe(
        "File must be at most 1 lines long. It's 7 lines long."
      )
    }
    else {
      expect(messages[1].message).toBe("File must be at most 1 lines long")
    }
    expect(messages[1].line).toBe(1)
    expect(messages[1].column).toBe(9)
  })
})

describe("html/report-bad-indent setting", () => {
  it("should report under-indented code with auto indent setting", () => {
    const messages = execute("report-bad-indent-setting.html", {
      settings: {
        "html/report-bad-indent": true,
      },
    })

    expect(messages.length).toBe(1)

    expect(messages[0].message).toBe("Bad line indentation.")
    expect(messages[0].line).toBe(10)
    expect(messages[0].column).toBe(1)
  })

  it("should report under-indented code with provided indent setting", () => {
    const messages = execute("report-bad-indent-setting.html", {
      settings: {
        "html/report-bad-indent": true,
        "html/indent": "+4",
      },
    })

    expect(messages.length).toBe(3)

    expect(messages[0].message).toBe("Bad line indentation.")
    expect(messages[0].line).toBe(9)
    expect(messages[0].column).toBe(1)

    expect(messages[1].message).toBe("Bad line indentation.")
    expect(messages[1].line).toBe(10)
    expect(messages[1].column).toBe(1)

    expect(messages[2].message).toBe("Bad line indentation.")
    expect(messages[2].line).toBe(11)
    expect(messages[2].column).toBe(1)
  })
})

describe("xml support", () => {
  it("consider .html files as HTML", () => {
    const messages = execute("cdata.html")

    expect(messages.length).toBe(1)

    expect(messages[0].message).toBe("Parsing error: Unexpected token <")
    expect(messages[0].fatal).toBe(true)
    expect(messages[0].line).toBe(10)
    expect(messages[0].column).toBe(7)
  })

  it("can be forced to consider .html files as XML", () => {
    const messages = execute("cdata.html", {
      settings: {
        "html/xml-extensions": [".html"],
      },
    })

    expect(messages.length).toBe(1)

    expect(messages[0].message).toBe("Unexpected console statement.")
    expect(messages[0].line).toBe(11)
    expect(messages[0].column).toBe(9)
  })

  it("consider .xhtml files as XML", () => {
    const messages = execute("cdata.xhtml")

    expect(messages.length).toBe(1)

    expect(messages[0].message).toBe("Unexpected console statement.")
    expect(messages[0].line).toBe(13)
    expect(messages[0].column).toBe(9)
  })

  it("can be forced to consider .xhtml files as HTML", () => {
    const messages = execute("cdata.xhtml", {
      settings: {
        "html/html-extensions": [".xhtml"],
      },
    })

    expect(messages.length).toBe(1)

    expect(messages[0].message).toBe("Parsing error: Unexpected token <")
    expect(messages[0].fatal).toBe(true)
    expect(messages[0].line).toBe(12)
    expect(messages[0].column).toBe(7)
  })

  it("removes white space at the end of scripts ending with CDATA", () => {
    const messages = execute("cdata.xhtml", {
      rules: {
        "no-console": "off",
        "no-trailing-spaces": "error",
        "eol-last": "error",
      },
    })

    expect(messages.length).toBe(0)
  })

  it("should support self closing script tags", () => {
    let messages
    expect(() => {
      messages = execute("self-closing-tags.xhtml")
    }).not.toThrow()
    expect(messages.length).toBe(0)
  })
})

describe("lines-around-comment and multiple scripts", () => {
  it("should not warn with lines-around-comment if multiple scripts", () => {
    const messages = execute("simple.html", {
      rules: {
        "lines-around-comment": ["error", { beforeLineComment: true }],
      },
    })

    expect(messages.length).toBe(5)
  })
})

describe("fix", () => {
  it("should remap fix ranges", () => {
    const messages = execute("fix.html", {
      rules: {
        "no-extra-semi": ["error"],
      },
    })

    if (isESLintVersion(">= 3.17.0")) {
      // Since v3.17.0, no-extra-semi replaces all semicolons by a single semi colon instead of
      // removing extra semi colons. See https://github.com/eslint/eslint/pull/8067 .
      expect(messages[0].fix.range).toEqual([53, 55])
    }
    else {
      expect(messages[0].fix.range).toEqual([54, 55])
    }
  })

  it("should fix errors", () => {
    const result = execute("fix.html", {
      rules: {
        "no-extra-semi": ["error"],
      },
      fix: true,
    })

    expect(result.output).toBe(`<!DOCTYPE html>
<html lang="en">
  <script>
    foo();
  </script>
</html>
`)
    expect(result.messages.length).toBe(0)
  })

  it("should fix errors in files with BOM", () => {
    const result = execute("fix-bom.html", {
      rules: {
        "no-extra-semi": ["error"],
      },
      fix: true,
    })

    expect(result.output).toBe(`\uFEFF<!DOCTYPE html>
<html lang="en">
  <script>
    foo();
  </script>
</html>
`)
    expect(result.messages.length).toBe(0)
  })

  describe("eol-last rule", () => {
    it("should work with eol-last always", () => {
      const result = execute("fix.html", {
        rules: {
          "eol-last": ["error"],
          "no-extra-semi": ["error"],
        },
        fix: true,
      })

      expect(result.output).toBe(`<!DOCTYPE html>
<html lang="en">
  <script>
    foo();
  </script>
</html>
`)
      expect(result.messages.length).toBe(0)
    })

    it("should work with eol-last never", () => {
      // ESLint 2 did not remove the last new line if any
      if (isESLintVersion("2")) return
      const result = execute("fix.html", {
        rules: {
          "eol-last": ["error", "never"],
        },
        fix: true,
      })

      expect(result.output).toBe(`<!DOCTYPE html>
<html lang="en">
  <script>
    foo();;  </script>
</html>
`)
      expect(result.messages.length).toBe(0)
    })
  })
})

describe("html/javascript-mime-types", () => {
  it("ignores unknown mime types by default", () => {
    const messages = execute("javascript-mime-types.html")

    expect(messages.length).toBe(2)

    expect(messages[0].ruleId).toBe("no-console")
    expect(messages[0].line).toBe(8)

    expect(messages[1].ruleId).toBe("no-console")
    expect(messages[1].line).toBe(12)
  })

  it("specifies a list of valid mime types", () => {
    const messages = execute("javascript-mime-types.html", {
      settings: {
        "html/javascript-mime-types": ["text/foo"],
      },
    })

    expect(messages.length, 2)

    expect(messages[0].ruleId).toBe("no-console")
    expect(messages[0].line).toBe(8)

    expect(messages[1].ruleId).toBe("no-console")
    expect(messages[1].line).toBe(16)
  })

  it("specifies a regexp of valid mime types", () => {
    const messages = execute("javascript-mime-types.html", {
      settings: {
        "html/javascript-mime-types": "/^(application|text)/foo$/",
      },
    })

    expect(messages.length).toBe(3)

    expect(messages[0].ruleId).toBe("no-console")
    expect(messages[0].line).toBe(8)

    expect(messages[1].ruleId).toBe("no-console")
    expect(messages[1].line).toBe(16)

    expect(messages[2].ruleId).toBe("no-console")
    expect(messages[2].line).toBe(20)
  })
})

it("should report correct eol-last message position", () => {
  const messages = execute("eol-last.html", {
    rules: {
      "eol-last": "error",
    },
  })

  expect(messages.length).toBe(1)

  expect(messages[0].ruleId).toBe("eol-last")
  expect(messages[0].line).toBe(6)
  if (isESLintVersion("> 2")) {
    expect(messages[0].column).toBe(42)
  }
  else {
    expect(messages[0].column).toBe(12)
  }
})
