// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package tfdiags

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/hcl/v2"
)

func TestBuild(t *testing.T) {
	type diagFlat struct {
		Severity Severity
		Summary  string
		Detail   string
		Subject  *SourceRange
		Context  *SourceRange
	}

	tests := map[string]struct {
		Cons func(Diagnostics) Diagnostics
		Want []diagFlat
	}{
		"nil": {
			func(diags Diagnostics) Diagnostics {
				return diags
			},
			nil,
		},
		"fmt.Errorf": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(fmt.Errorf("oh no bad"))
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "oh no bad",
				},
			},
		},
		"errors.New": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(errors.New("oh no bad"))
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "oh no bad",
				},
			},
		},
		"hcl.Diagnostic": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
						End:      hcl.Pos{Line: 2, Column: 3, Byte: 25},
					},
					Context: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 3, Column: 1, Byte: 30},
					},
				})
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 1, Column: 10, Byte: 9},
						End:      SourcePos{Line: 2, Column: 3, Byte: 25},
					},
					Context: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 1, Column: 1, Byte: 0},
						End:      SourcePos{Line: 3, Column: 1, Byte: 30},
					},
				},
			},
		},
		"hcl.Diagnostics": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(hcl.Diagnostics{
					{
						Severity: hcl.DiagError,
						Summary:  "Something bad happened",
						Detail:   "It was really, really bad.",
					},
					{
						Severity: hcl.DiagWarning,
						Summary:  "Also, somebody sneezed",
						Detail:   "How rude!",
					},
				})
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
				},
				{
					Severity: Warning,
					Summary:  "Also, somebody sneezed",
					Detail:   "How rude!",
				},
			},
		},
		"errors.Join": {
			func(diags Diagnostics) Diagnostics {
				err := errors.Join(nil, errors.New("bad thing A"))
				err = errors.Join(err, errors.New("bad thing B"))
				diags = diags.Append(err)
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "bad thing A",
				},
				{
					Severity: Error,
					Summary:  "bad thing B",
				},
			},
		},
		"concat Diagnostics": {
			func(diags Diagnostics) Diagnostics {
				var moreDiags Diagnostics
				moreDiags = moreDiags.Append(errors.New("bad thing A"))
				moreDiags = moreDiags.Append(errors.New("bad thing B"))
				return diags.Append(moreDiags)
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "bad thing A",
				},
				{
					Severity: Error,
					Summary:  "bad thing B",
				},
			},
		},
		"Diagnostics.Err": {
			func(diags Diagnostics) Diagnostics {
				var moreDiags Diagnostics
				moreDiags = moreDiags.Append(errors.New("bad thing A"))
				moreDiags = moreDiags.Append(errors.New("bad thing B"))
				return diags.Append(moreDiags.Err())
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "bad thing A",
				},
				{
					Severity: Error,
					Summary:  "bad thing B",
				},
			},
		},
		"Diagnostics.ErrWithWarnings": {
			func(diags Diagnostics) Diagnostics {
				var moreDiags Diagnostics
				moreDiags = moreDiags.Append(SimpleWarning("Don't forget your toothbrush!"))
				moreDiags = moreDiags.Append(SimpleWarning("Always make sure you know where your towel is"))
				return diags.Append(moreDiags.ErrWithWarnings())
			},
			[]diagFlat{
				{
					Severity: Warning,
					Summary:  "Don't forget your toothbrush!",
				},
				{
					Severity: Warning,
					Summary:  "Always make sure you know where your towel is",
				},
			},
		},
		"single Diagnostic": {
			func(diags Diagnostics) Diagnostics {
				return diags.Append(SimpleWarning("Don't forget your toothbrush!"))
			},
			[]diagFlat{
				{
					Severity: Warning,
					Summary:  "Don't forget your toothbrush!",
				},
			},
		},
		"multiple appends": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(SimpleWarning("Don't forget your toothbrush!"))
				diags = diags.Append(fmt.Errorf("exploded"))
				return diags
			},
			[]diagFlat{
				{
					Severity: Warning,
					Summary:  "Don't forget your toothbrush!",
				},
				{
					Severity: Error,
					Summary:  "exploded",
				},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			gotDiags := test.Cons(nil)
			var got []diagFlat
			for _, item := range gotDiags {
				desc := item.Description()
				source := item.Source()
				got = append(got, diagFlat{
					Severity: item.Severity(),
					Summary:  desc.Summary,
					Detail:   desc.Detail,
					Subject:  source.Subject,
					Context:  source.Context,
				})
			}

			if !reflect.DeepEqual(got, test.Want) {
				t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(test.Want))
			}
		})
	}
}

func TestDiagnosticsErr(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		var diags Diagnostics
		err := diags.Err()
		if err != nil {
			t.Errorf("got non-nil error %#v; want nil", err)
		}
	})
	t.Run("warning only", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(SimpleWarning("bad"))
		err := diags.Err()
		if err != nil {
			t.Errorf("got non-nil error %#v; want nil", err)
		}
	})
	t.Run("one error", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		err := diags.Err()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		if got, want := err.Error(), "didn't work"; got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
	})
	t.Run("two errors", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		diags = diags.Append(errors.New("didn't work either"))
		err := diags.Err()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		want := strings.TrimSpace(`
2 problems:

- didn't work
- didn't work either
`)
		if got := err.Error(); got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
	})
	t.Run("error and warning", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		diags = diags.Append(SimpleWarning("didn't work either"))
		err := diags.Err()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		// Since this "as error" mode is just a fallback for
		// non-diagnostics-aware situations like tests, we don't actually
		// distinguish warnings and errors here since the point is to just
		// get the messages rendered. User-facing code should be printing
		// each diagnostic separately, so won't enter this codepath,
		want := strings.TrimSpace(`
2 problems:

- didn't work
- didn't work either
`)
		if got := err.Error(); got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
	})
}

func TestDiagnosticsErrWithWarnings(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		var diags Diagnostics
		err := diags.ErrWithWarnings()
		if err != nil {
			t.Errorf("got non-nil error %#v; want nil", err)
		}
	})
	t.Run("warning only", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(SimpleWarning("bad"))
		err := diags.ErrWithWarnings()
		if err == nil {
			t.Errorf("got nil error; want NonFatalError")
			return
		}
		if _, ok := err.(NonFatalError); !ok {
			t.Errorf("got %T; want NonFatalError", err)
		}
	})
	t.Run("one error", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		err := diags.ErrWithWarnings()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		if got, want := err.Error(), "didn't work"; got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
	})
	t.Run("two errors", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		diags = diags.Append(errors.New("didn't work either"))
		err := diags.ErrWithWarnings()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		want := strings.TrimSpace(`
2 problems:

- didn't work
- didn't work either
`)
		if got := err.Error(); got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
	})
	t.Run("error and warning", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		diags = diags.Append(SimpleWarning("didn't work either"))
		err := diags.ErrWithWarnings()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		// Since this "as error" mode is just a fallback for
		// non-diagnostics-aware situations like tests, we don't actually
		// distinguish warnings and errors here since the point is to just
		// get the messages rendered. User-facing code should be printing
		// each diagnostic separately, so won't enter this codepath,
		want := strings.TrimSpace(`
2 problems:

- didn't work
- didn't work either
`)
		if got := err.Error(); got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
	})
}

func TestDiagnosticsNonFatalErr(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		var diags Diagnostics
		err := diags.NonFatalErr()
		if err != nil {
			t.Errorf("got non-nil error %#v; want nil", err)
		}
	})
	t.Run("warning only", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(SimpleWarning("bad"))
		err := diags.NonFatalErr()
		if err == nil {
			t.Errorf("got nil error; want NonFatalError")
			return
		}
		if _, ok := err.(NonFatalError); !ok {
			t.Errorf("got %T; want NonFatalError", err)
		}
	})
	t.Run("one error", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		err := diags.NonFatalErr()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		if got, want := err.Error(), "didn't work"; got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
		if _, ok := err.(NonFatalError); !ok {
			t.Errorf("got %T; want NonFatalError", err)
		}
	})
	t.Run("two errors", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		diags = diags.Append(errors.New("didn't work either"))
		err := diags.NonFatalErr()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		want := strings.TrimSpace(`
2 problems:

- didn't work
- didn't work either
`)
		if got := err.Error(); got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
		if _, ok := err.(NonFatalError); !ok {
			t.Errorf("got %T; want NonFatalError", err)
		}
	})
	t.Run("error and warning", func(t *testing.T) {
		var diags Diagnostics
		diags = diags.Append(errors.New("didn't work"))
		diags = diags.Append(SimpleWarning("didn't work either"))
		err := diags.NonFatalErr()
		if err == nil {
			t.Fatalf("got nil error %#v; want non-nil", err)
		}
		// Since this "as error" mode is just a fallback for
		// non-diagnostics-aware situations like tests, we don't actually
		// distinguish warnings and errors here since the point is to just
		// get the messages rendered. User-facing code should be printing
		// each diagnostic separately, so won't enter this codepath,
		want := strings.TrimSpace(`
2 problems:

- didn't work
- didn't work either
`)
		if got := err.Error(); got != want {
			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
		}
		if _, ok := err.(NonFatalError); !ok {
			t.Errorf("got %T; want NonFatalError", err)
		}
	})
}

func TestWarnings(t *testing.T) {
	errorDiag := &hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "something bad happened",
		Detail:   "details of the error",
	}

	warnDiag := &hcl.Diagnostic{
		Severity: hcl.DiagWarning,
		Summary:  "something bad happened",
		Detail:   "details of the warning",
	}

	cases := map[string]struct {
		diags    Diagnostics
		expected Diagnostics
	}{
		"empty diags": {
			diags:    Diagnostics{},
			expected: Diagnostics{},
		},
		"nil diags": {
			diags:    nil,
			expected: Diagnostics{},
		},
		"all error diags": {
			diags: func() Diagnostics {
				var d Diagnostics
				d = d.Append(errorDiag, errorDiag, errorDiag)
				return d
			}(),
			expected: Diagnostics{},
		},
		"mixture of error and warning diags": {
			diags: func() Diagnostics {
				var d Diagnostics
				d = d.Append(errorDiag, errorDiag, warnDiag)
				return d
			}(),
			expected: func() Diagnostics {
				var d Diagnostics
				d = d.Append(warnDiag)
				return d
			}(),
		},
		"empty error diags": {
			diags: func() Diagnostics {
				var d Diagnostics
				d = d.Append(warnDiag, warnDiag)
				return d
			}(),
			expected: func() Diagnostics {
				var d Diagnostics
				d = d.Append(warnDiag, warnDiag)
				return d
			}(),
		},
	}

	for tn, tc := range cases {
		t.Run(tn, func(t *testing.T) {
			warnings := tc.diags.Warnings()

			AssertDiagnosticsMatch(t, tc.expected, warnings)
		})
	}
}

func TestAppendWithoutDuplicates(t *testing.T) {
	type diagFlat struct {
		Severity Severity
		Summary  string
		Detail   string
		Subject  *SourceRange
		Context  *SourceRange
	}

	tests := map[string]struct {
		Cons func(Diagnostics) Diagnostics
		Want []diagFlat
	}{
		"nil": {
			func(diags Diagnostics) Diagnostics {
				return nil
			},
			nil,
		},
		"errors.New": {
			// these could be from different locations, so we can't dedupe them
			func(diags Diagnostics) Diagnostics {
				return diags.Append(
					errors.New("oh no bad"),
					errors.New("oh no bad"),
				)
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "oh no bad",
				},
				{
					Severity: Error,
					Summary:  "oh no bad",
				},
			},
		},
		"hcl.Diagnostic": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
						End:      hcl.Pos{Line: 2, Column: 3, Byte: 25},
					},
					Context: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 3, Column: 1, Byte: 30},
					},
				})
				// exact same diag
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
						End:      hcl.Pos{Line: 2, Column: 3, Byte: 25},
					},
					Context: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 3, Column: 1, Byte: 30},
					},
				})
				// same diag as prev, different location
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 4, Column: 10, Byte: 40},
						End:      hcl.Pos{Line: 5, Column: 3, Byte: 55},
					},
					Context: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 4, Column: 1, Byte: 40},
						End:      hcl.Pos{Line: 6, Column: 1, Byte: 60},
					},
				})
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 1, Column: 10, Byte: 9},
						End:      SourcePos{Line: 2, Column: 3, Byte: 25},
					},
					Context: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 1, Column: 1, Byte: 0},
						End:      SourcePos{Line: 3, Column: 1, Byte: 30},
					},
				},
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 4, Column: 10, Byte: 40},
						End:      SourcePos{Line: 5, Column: 3, Byte: 55},
					},
					Context: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 4, Column: 1, Byte: 40},
						End:      SourcePos{Line: 6, Column: 1, Byte: 60},
					},
				},
			},
		},
		"simple warning": {
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(SimpleWarning("Don't forget your toothbrush!"))
				diags = diags.Append(SimpleWarning("Don't forget your toothbrush!"))
				return diags
			},
			[]diagFlat{
				{
					Severity: Warning,
					Summary:  "Don't forget your toothbrush!",
				},
				{
					Severity: Warning,
					Summary:  "Don't forget your toothbrush!",
				},
			},
		},
		"hcl.Diagnostic extra": {
			// Extra can contain anything, and we don't know how to compare
			// those values, so we can't dedupe them
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Extra:    42,
					Subject: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
						End:      hcl.Pos{Line: 2, Column: 3, Byte: 25},
					},
				})
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Extra:    38,
					Subject: &hcl.Range{
						Filename: "foo.tf",
						Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
						End:      hcl.Pos{Line: 2, Column: 3, Byte: 25},
					},
				})
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 1, Column: 10, Byte: 9},
						End:      SourcePos{Line: 2, Column: 3, Byte: 25},
					},
				},
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
					Subject: &SourceRange{
						Filename: "foo.tf",
						Start:    SourcePos{Line: 1, Column: 10, Byte: 9},
						End:      SourcePos{Line: 2, Column: 3, Byte: 25},
					},
				},
			},
		},
		"hcl.Diagnostic no-location": {
			// Extra can contain anything, and we don't know how to compare
			// those values, so we can't dedupe them
			func(diags Diagnostics) Diagnostics {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
				})
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
				})
				return diags
			},
			[]diagFlat{
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
				},
				{
					Severity: Error,
					Summary:  "Something bad happened",
					Detail:   "It was really, really bad.",
				},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			var deduped Diagnostics

			diags := test.Cons(nil)
			deduped = deduped.AppendWithoutDuplicates(diags...)

			var got []diagFlat
			for _, item := range deduped {
				desc := item.Description()
				source := item.Source()
				got = append(got, diagFlat{
					Severity: item.Severity(),
					Summary:  desc.Summary,
					Detail:   desc.Detail,
					Subject:  source.Subject,
					Context:  source.Context,
				})
			}

			if !reflect.DeepEqual(got, test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}
