package hugolib

import (
	"fmt"
	"path/filepath"
	"strings"
	"testing"

	qt "github.com/frankban/quicktest"
	"github.com/gohugoio/hugo/common/herrors"
)

type testSiteBuildErrorAsserter struct {
	name string
	c    *qt.C
}

func (t testSiteBuildErrorAsserter) getFileError(err error) herrors.FileError {
	t.c.Assert(err, qt.Not(qt.IsNil), qt.Commentf(t.name))
	fe := herrors.UnwrapFileError(err)
	t.c.Assert(fe, qt.Not(qt.IsNil))
	return fe
}

func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) {
	t.c.Helper()
	fe := t.getFileError(err)
	t.c.Assert(fe.Position().LineNumber, qt.Equals, lineNumber, qt.Commentf(err.Error()))
}

func (t testSiteBuildErrorAsserter) assertErrorMessage(e1, e2 string) {
	// The error message will contain filenames with OS slashes. Normalize before compare.
	e1, e2 = filepath.ToSlash(e1), filepath.ToSlash(e2)
	t.c.Assert(e2, qt.Contains, e1)
}

func TestSiteBuildErrors(t *testing.T) {
	const (
		yamlcontent = "yamlcontent"
		tomlcontent = "tomlcontent"
		jsoncontent = "jsoncontent"
		shortcode   = "shortcode"
		base        = "base"
		single      = "single"
	)

	type testCase struct {
		name      string
		fileType  string
		fileFixer func(content string) string
		assertErr func(a testSiteBuildErrorAsserter, err error)
	}

	createTestFiles := func(tc testCase) string {
		f := func(ftype, content string) string {
			if ftype != tc.fileType {
				return content
			}
			return tc.fileFixer(content)
		}

		return `
-- hugo.toml --
baseURL = "https://example.com"
-- layouts/_shortcodes/sc.html --
` + f(shortcode, `SHORTCODE L1
SHORTCODE L2
SHORTCODE L3:
SHORTCODE L4: {{ .Page.Title }}
`) + `
-- layouts/baseof.html --
` + f(base, `BASEOF L1
BASEOF L2
BASEOF L3
BASEOF L4{{ if .Title }}{{ end }}
{{block "main" .}}This is the main content.{{end}}
BASEOF L6
`) + `
-- layouts/single.html --
` + f(single, `{{ define "main" }}
SINGLE L2:
SINGLE L3:
SINGLE L4:
SINGLE L5: {{ .Title }} {{ .Content }}
{{ end }}
`) + `
-- layouts/foo/single.html --
` + f(single, `
SINGLE L2:
SINGLE L3:
SINGLE L4:
SINGLE L5: {{ .Title }} {{ .Content }}
`) + `
-- content/myyaml.md --
` + f(yamlcontent, `---
title: "The YAML"
---

Some content.

         {{< sc >}}

Some more text.

The end.
`) + `
-- content/mytoml.md --
` + f(tomlcontent, `+++
title = "The TOML"
p1 = "v"
p2 = "v"
p3 = "v"
description = "Descriptioon"
+++

Some content.
`) + `
-- content/myjson.md --
` + f(jsoncontent, `{
	"title": "This is a title",
	"description": "This is a description."
}

Some content.
`)
	}

	tests := []testCase{
		{
			name:     "Base template parse failed",
			fileType: base,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title }}", ".Title }", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				a.assertLineNumber(4, err)
			},
		},
		{
			name:     "Base template execute failed",
			fileType: base,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title", ".Titles", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				a.assertLineNumber(4, err)
			},
		},
		{
			name:     "Single template parse failed",
			fileType: single,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title }}", ".Title }", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
				a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 1)
				a.assertErrorMessage("\"/layouts/foo/single.html:5:1\": parse of template failed: template: foo/single.html:5: unexpected \"}\" in operand", fe.Error())
			},
		},
		{
			name:     "Single template execute failed",
			fileType: single,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title", ".Titles", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
				a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14)
				a.assertErrorMessage("\"layouts/single.html:5:14\": execute of template failed", fe.Error())
			},
		},
		{
			name:     "Single template execute failed, long keyword",
			fileType: single,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title", ".ThisIsAVeryLongTitle", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
				a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14)
				a.assertErrorMessage("\"layouts/single.html:5:14\": execute of template failed", fe.Error())
			},
		},
		{
			name:     "Shortcode parse failed",
			fileType: shortcode,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title }}", ".Title }", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				a.assertLineNumber(4, err)
			},
		},
		{
			name:     "Shortcode execute failed",
			fileType: shortcode,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title", ".Titles", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				// Make sure that it contains both the content file and template
				a.assertErrorMessage(`"content/myyaml.md:7:10": failed to render shortcode "sc": failed to process shortcode: "layouts/_shortcodes/sc.html:4:22": execute of template failed: template: shortcodes/sc.html:4:22: executing "shortcodes/sc.html" at <.Page.Titles>: can't evaluate field Titles in type page.Page`, fe.Error())
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 7)
			},
		},
		{
			name:     "Shortode does not exist",
			fileType: yamlcontent,
			fileFixer: func(content string) string {
				return strings.Replace(content, "{{< sc >}}", "{{< nono >}}", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 7)
				a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 10)
				a.assertErrorMessage(`"content/myyaml.md:7:10": failed to extract shortcode: template for shortcode "nono" not found`, fe.Error())
			},
		},
		{
			name:     "Invalid YAML front matter",
			fileType: yamlcontent,
			fileFixer: func(content string) string {
				return `---
title: "My YAML Content"
foo bar
---
`
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				a.assertLineNumber(3, err)
			},
		},
		{
			name:     "Invalid TOML front matter",
			fileType: tomlcontent,
			fileFixer: func(content string) string {
				return strings.Replace(content, "description = ", "description &", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 6)
			},
		},
		{
			name:     "Invalid JSON front matter",
			fileType: jsoncontent,
			fileFixer: func(content string) string {
				return strings.Replace(content, "\"description\":", "\"description\"", 1)
			},
			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 3)
			},
		},
		{
			// See https://github.com/gohugoio/hugo/issues/5327
			name:     "Panic in template Execute",
			fileType: single,
			fileFixer: func(content string) string {
				return strings.Replace(content, ".Title", ".Parent.Parent.Parent", 1)
			},

			assertErr: func(a testSiteBuildErrorAsserter, err error) {
				a.c.Assert(err, qt.Not(qt.IsNil))
				fe := a.getFileError(err)
				a.c.Assert(fe.Position().LineNumber, qt.Equals, 5)
				a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 21)
			},
		},
	}

	for _, test := range tests {
		test := test
		if test.name != "Base template parse failed" {
			continue
		}
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			c := qt.New(t)
			errorAsserter := testSiteBuildErrorAsserter{
				c:    c,
				name: test.name,
			}

			files := createTestFiles(test)

			_, err := TestE(t, files)

			if test.assertErr != nil {
				test.assertErr(errorAsserter, err)
			} else {
				c.Assert(err, qt.IsNil)
			}
		})
	}
}

// Issue 9852
func TestErrorMinify(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
[minify]
minifyOutput = true

-- layouts/home.html --
<body>
<script>=;</script>
</body>

`

	b, err := TestE(t, files)

	b.Assert(err, qt.IsNotNil)
	fes := herrors.UnwrapFileErrors(err)
	b.Assert(len(fes), qt.Equals, 1)
	fe := fes[0]
	b.Assert(fe, qt.IsNotNil)
	b.Assert(fe.Position().LineNumber, qt.Equals, 2)
	b.Assert(fe.Position().ColumnNumber, qt.Equals, 9)
	b.Assert(fe.Error(), qt.Contains, "unexpected = in expression on line 2 and column 9")
	b.Assert(filepath.ToSlash(fe.Position().Filename), qt.Contains, "hugo-transform-error")
	// os.Remove is not needed in txtar tests as the filesystem is ephemeral.
	// b.Assert(os.Remove(fe.Position().Filename), qt.IsNil)
}

func TestErrorNestedRender(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---
-- layouts/home.html --
line 1
line 2
1{{ .Render "myview" }}
-- layouts/myview.html --
line 1
12{{ partial "foo.html" . }}
line 4
line 5
-- layouts/_partials/foo.html --
line 1
line 2
123{{ .ThisDoesNotExist }}
line 4
`

	b, err := TestE(t, files)

	b.Assert(err, qt.IsNotNil)
	errors := herrors.UnwrapFileErrorsWithErrorContext(err)
	b.Assert(errors, qt.HasLen, 4)
	b.Assert(errors[0].Position().LineNumber, qt.Equals, 3)
	b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 4)
	b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/layouts/home.html:3:4": execute of template failed`))
	b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "1{{ .Render \"myview\" }}"})
	b.Assert(errors[2].Position().LineNumber, qt.Equals, 2)
	b.Assert(errors[2].Position().ColumnNumber, qt.Equals, 5)
	b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"})

	b.Assert(errors[3].Position().LineNumber, qt.Equals, 3)
	b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6)
	b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"})
}

func TestErrorNestedShortcode(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---

## Hello
{{< hello >}}

-- layouts/home.html --
line 1
line 2
{{ .Content }}
line 5
-- layouts/_shortcodes/hello.html --
line 1
12{{ partial "foo.html" . }}
line 4
line 5
-- layouts/_partials/foo.html --
line 1
line 2
123{{ .ThisDoesNotExist }}
line 4
`

	b, err := TestE(t, files)

	b.Assert(err, qt.IsNotNil)
	errors := herrors.UnwrapFileErrorsWithErrorContext(err)

	b.Assert(errors, qt.HasLen, 4)

	b.Assert(errors[1].Position().LineNumber, qt.Equals, 6)
	b.Assert(errors[1].Position().ColumnNumber, qt.Equals, 1)
	b.Assert(errors[1].ErrorContext().ChromaLexer, qt.Equals, "md")
	b.Assert(errors[1].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:6:1": failed to render shortcode "hello": failed to process shortcode: "/layouts/_shortcodes/hello.html:2:5":`))
	b.Assert(errors[1].ErrorContext().Lines, qt.DeepEquals, []string{"", "## Hello", "{{< hello >}}", ""})
	b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"})
	b.Assert(errors[3].Position().LineNumber, qt.Equals, 3)
	b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6)
	b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"})
}

func TestErrorRenderHookHeading(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---

## Hello

-- layouts/home.html --
line 1
line 2
{{ .Content }}
line 5
-- layouts/_markup/render-heading.html --
line 1
12{{ .Levels }}
line 4
line 5
`

	b, err := TestE(t, files)

	b.Assert(err, qt.IsNotNil)
	errors := herrors.UnwrapFileErrorsWithErrorContext(err)

	b.Assert(errors, qt.HasLen, 3)
	b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:2:5": "/layouts/_markup/render-heading.html:2:5": execute of template failed`))
}

func TestErrorRenderHookCodeblock(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---

## Hello

§§§ foo
bar
§§§


-- layouts/home.html --
line 1
line 2
{{ .Content }}
line 5
-- layouts/_markup/render-codeblock-foo.html --
line 1
12{{ .Foo }}
line 4
line 5
`

	b, err := TestE(t, files)

	b.Assert(err, qt.IsNotNil)
	errors := herrors.UnwrapFileErrorsWithErrorContext(err)

	b.Assert(errors, qt.HasLen, 3)
	first := errors[0]
	b.Assert(first.Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:7:1": "/layouts/_markup/render-codeblock-foo.html:2:5": execute of template failed`))
}

func TestErrorInBaseTemplate(t *testing.T) {
	t.Parallel()

	filesTemplate := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---
-- layouts/baseof.html --
line 1 base
line 2 base
{{ block "main" . }}empty{{ end }}
line 4 base
{{ block "toc" . }}empty{{ end }}
-- layouts/home.html --
{{ define "main" }}
line 2 index
line 3 index
line 4 index
{{ end }}
{{ define "toc" }}
TOC: {{ partial "toc.html" . }}
{{ end }}
-- layouts/_partials/toc.html --
toc line 1
toc line 2
toc line 3
toc line 4


`

	t.Run("base template", func(t *testing.T) {
		files := strings.Replace(filesTemplate, "line 4 base", "123{{ .ThisDoesNotExist \"abc\" }}", 1)

		b, err := TestE(t, files)

		b.Assert(err, qt.IsNotNil)
		b.Assert(err.Error(), qt.Contains, `baseof.html:4:6`)
	})

	t.Run("home template", func(t *testing.T) {
		files := strings.Replace(filesTemplate, "line 3 index", "1234{{ .ThisDoesNotExist \"abc\" }}", 1)

		b, err := TestE(t, files)

		b.Assert(err, qt.IsNotNil)
		b.Assert(err.Error(), qt.Contains, `home.html:3:7"`)
	})

	t.Run("partial from define", func(t *testing.T) {
		files := strings.Replace(filesTemplate, "toc line 2", "12345{{ .ThisDoesNotExist \"abc\" }}", 1)

		b, err := TestE(t, files)

		b.Assert(err, qt.IsNotNil)
		b.Assert(err.Error(), qt.Contains, `toc.html:2:8"`)
	})
}

// https://github.com/gohugoio/hugo/issues/5375
func TestSiteBuildTimeout(t *testing.T) {
	t.Parallel()

	var filesBuilder strings.Builder
	filesBuilder.WriteString(`
-- hugo.toml --
timeout = '100ms'
-- layouts/single.html --
{{ .WordCount }}
-- layouts/_shortcodes/c.html --
{{ range .Page.Site.RegularPages }}
{{ .WordCount }}
{{ end }}
`)

	for i := range 20 {
		filesBuilder.WriteString(fmt.Sprintf(`
-- content/page%d.md --
---
title: "A page"
---

{{< c >}}
`, i))
	}

	_, err := TestE(t, filesBuilder.String())

	qt.Assert(t, err, qt.Not(qt.IsNil))
	qt.Assert(t, err.Error(), qt.Contains, "timed out rendering the page content")
}

func TestErrorTemplateRuntime(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
-- layouts/home.html --
Home.
{{ .ThisDoesNotExist }}
 `

	b, err := TestE(t, files)

	b.Assert(err, qt.Not(qt.IsNil))
	b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/home.html:2:3`))
	b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`)
}

func TestErrorFrontmatterYAMLSyntax(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
-- content/_index.md --





---
line1: 'value1'
x
line2: 'value2'
line3: 'value3'
---	
`

	b, err := TestE(t, files)

	b.Assert(err, qt.Not(qt.IsNil))
	b.Assert(err.Error(), qt.Contains, "[2:1] non-map value is specified")
	fes := herrors.UnwrapFileErrors(err)
	b.Assert(len(fes), qt.Equals, 1)
	fe := fes[0]
	b.Assert(fe, qt.Not(qt.IsNil))
	pos := fe.Position()
	b.Assert(pos.Filename, qt.Contains, filepath.FromSlash("content/_index.md"))
	b.Assert(fe.ErrorContext(), qt.Not(qt.IsNil))
	b.Assert(pos.LineNumber, qt.Equals, 8)
	b.Assert(pos.ColumnNumber, qt.Equals, 1)
}
