// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package views

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

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/hcl/v2"
	"github.com/opentofu/opentofu/internal/command/arguments"
	"github.com/opentofu/opentofu/internal/terminal"
	"github.com/opentofu/opentofu/internal/tfdiags"
	"github.com/opentofu/opentofu/internal/tofu"
)

// TestNewView it's just a sanity check to be sure that we have it initialized as expected.
func TestNewView(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	view := NewView(streams)

	if view.streams != streams {
		t.Error("expected streams to be set")
	}
	if view.colorize == nil {
		t.Error("expected colorize to be initialized")
	}
	if !view.colorize.Disable {
		t.Error("expected colorize to be disabled by default")
	}
	if view.configSources == nil {
		t.Error("expected configSources to be initialized")
	}
	if view.diagsPrinter == nil {
		t.Error("expected diagsPrinter to be initialized")
	}
	// Test that configSources returns nil by default
	if sources := view.configSources(); sources != nil {
		t.Errorf("expected configSources to return nil by default, got %v", sources)
	}

	_ = done(t)
}

func TestView_Configure(t *testing.T) {
	testCases := map[string]struct {
		viewArgs *arguments.View
		validate func(*testing.T, *View)
	}{
		"no color": {

			viewArgs: &arguments.View{
				NoColor: true,
			},
			validate: func(t *testing.T, v *View) {
				if !v.colorize.Disable {
					t.Error("expected colorize to be disabled")
				}
			},
		},
		"with color": {

			viewArgs: &arguments.View{
				NoColor: false,
			},
			validate: func(t *testing.T, v *View) {
				if v.colorize.Disable {
					t.Error("expected colorize to be enabled")
				}
			},
		},
		"compact warnings": {

			viewArgs: &arguments.View{
				CompactWarnings: true,
			},
			validate: func(t *testing.T, v *View) {
				if !v.compactWarnings {
					t.Error("expected compactWarnings to be true")
				}
			},
		},
		"consolidate warnings": {

			viewArgs: &arguments.View{
				ConsolidateWarnings: true,
			},
			validate: func(t *testing.T, v *View) {
				if !v.consolidateWarnings {
					t.Error("expected consolidateWarnings to be true")
				}
			},
		},
		"consolidate errors": {

			viewArgs: &arguments.View{
				ConsolidateErrors: true,
			},
			validate: func(t *testing.T, v *View) {
				if !v.consolidateErrors {
					t.Error("expected consolidateErrors to be true")
				}
			},
		},
		"concise": {

			viewArgs: &arguments.View{
				Concise: true,
			},
			validate: func(t *testing.T, v *View) {
				if !v.concise {
					t.Error("expected concise to be true")
				}
			},
		},
		"module deprecation warning level": {

			viewArgs: &arguments.View{
				ModuleDeprecationWarnLvl: tofu.DeprecationWarningLevelLocal,
			},
			validate: func(t *testing.T, v *View) {
				if v.ModuleDeprecationWarnLvl != tofu.DeprecationWarningLevelLocal {
					t.Errorf("expected ModuleDeprecationWarnLvl to be Local, got %v", v.ModuleDeprecationWarnLvl)
				}
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			streams, done := terminal.StreamsForTesting(t)
			view := NewView(streams)
			view.Configure(tc.viewArgs)
			tc.validate(t, view)
			purpleVal, ok := view.colorize.Colors["purple"]
			const expectedPurpleVal = "38;5;57"
			if !ok {
				t.Error("expected purple color to be added to colorize colors")
			}
			if purpleVal != expectedPurpleVal {
				t.Errorf("expected purple color code to be %s, got %s", expectedPurpleVal, purpleVal)
			}

			_ = done(t)
		})
	}
}

func TestView_Diagnostics(t *testing.T) {
	testCases := map[string]struct {
		diags    tfdiags.Diagnostics
		setup    func(*View)
		validate func(*testing.T, *terminal.TestOutput)
	}{
		"empty diagnostics": {

			diags: tfdiags.Diagnostics{},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				if output.Stdout() != "" {
					t.Errorf("expected empty stdout, got %q", output.Stdout())
				}
				if output.Stderr() != "" {
					t.Errorf("expected empty stderr, got %q", output.Stderr())
				}
			},
		},
		"warning diagnostic": {

			diags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Test warning",
					"This is a test warning",
				),
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stdout := output.Stdout()
				if !strings.Contains(stdout, "Warning") {
					t.Errorf("expected stdout to contain 'Warning', got %q", stdout)
				}
				if !strings.Contains(stdout, "Test warning") {
					t.Errorf("expected stdout to contain 'Test warning', got %q", stdout)
				}
			},
		},
		"error diagnostic": {

			diags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Error,
					"Test error",
					"This is a test error",
				),
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stderr := output.Stderr()
				if !strings.Contains(stderr, "Error") {
					t.Errorf("expected stderr to contain 'Error', got %q", stderr)
				}
				if !strings.Contains(stderr, "Test error") {
					t.Errorf("expected stderr to contain 'Test error', got %q", stderr)
				}
			},
		},
		"multiple diagnostics": {

			diags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Warning 1",
					"First warning",
				),
				tfdiags.Sourceless(
					tfdiags.Error,
					"Error 1",
					"First error",
				),
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stdout := output.Stdout()
				stderr := output.Stderr()

				if !strings.Contains(stdout, "Warning 1") {
					t.Errorf("expected stdout to contain warning, got %q", stdout)
				}
				if !strings.Contains(stderr, "Error 1") {
					t.Errorf("expected stderr to contain error, got %q", stderr)
				}
			},
		},
		"multiple diagnostics with newline": {

			setup: func(view *View) {
				view.DiagsWithNewline()
			},
			diags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Warning 1",
					"First warning",
				),
				tfdiags.Sourceless(
					tfdiags.Error,
					"Error 1",
					"First error",
				),
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stdout := output.Stdout()
				stderr := output.Stderr()

				if !strings.Contains(stdout, "Warning 1") {
					t.Errorf("expected stdout to contain warning, got %q", stdout)
				}
				if !strings.HasSuffix(stdout, "\n") {
					t.Fatalf("expected stdout to end in new line after the warning diagnostic")
				}
				if !strings.Contains(stderr, "Error 1") {
					t.Errorf("expected stderr to contain error, got %q", stderr)
				}
				if !strings.HasSuffix(stderr, "\n") {
					t.Fatalf("expected sstderr to end in new line after the error diagnostic")
				}
			},
		},
		"compact warnings - warnings only": {

			diags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Warning 1",
					"First warning detail",
				),
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Warning 2",
					"Second warning detail",
				),
			},
			setup: func(v *View) {
				v.compactWarnings = true
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stdout := output.Stdout()
				if !strings.Contains(stdout, "Warnings:") {
					t.Errorf("expected compact warnings format, got %q", stdout)
				}
			},
		},
		"consolidate warnings": {

			diags: tfdiags.Diagnostics{}.
				Append(&hcl.Diagnostic{
					Severity: hcl.DiagWarning,
					Summary:  "Warning with source",
					Detail:   "foo bar warning",
					Subject:  hcl.RangeBetween(hcl.Range{Filename: "test.tf"}, hcl.Range{Filename: "test.tf"}).Ptr(),
				}).
				Append(&hcl.Diagnostic{
					Severity: hcl.DiagWarning,
					Summary:  "Warning with source",
					Detail:   "foo bar other warning",
					Subject:  hcl.RangeBetween(hcl.Range{Filename: "test.tf"}, hcl.Range{Filename: "test.tf"}).Ptr(),
				}),
			setup: func(v *View) {
				v.consolidateWarnings = true
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stdout := output.Stdout()
				if want, got := 1, strings.Count(stdout, "Warning with source"); want != got {
					t.Errorf("expected %d warnings in stdout, got %d\nstdout:\n%s", want, got, stdout)
				}
				if !strings.Contains(stdout, "and one more similar warning elsewhere") {
					t.Errorf("stdout should contain also the indication that the diagnostics have been consolidated")
				}
			},
		},
		"consolidate errors": {

			diags: tfdiags.Diagnostics{}.
				Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Error with source",
					Detail:   "foo bar error",
					Subject:  hcl.RangeBetween(hcl.Range{Filename: "test.tf"}, hcl.Range{Filename: "test.tf"}).Ptr(),
				}).
				Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Error with source",
					Detail:   "foo bar other error",
					Subject:  hcl.RangeBetween(hcl.Range{Filename: "test.tf"}, hcl.Range{Filename: "test.tf"}).Ptr(),
				}),
			setup: func(v *View) {
				v.consolidateErrors = true
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stderr := output.Stderr()
				if want, got := 1, strings.Count(stderr, "Error with source"); want != got {
					t.Errorf("expected %d errors in stderr, got %d\nstderr:\n%s", want, got, stderr)
				}
				if !strings.Contains(stderr, "and one more similar error elsewhere") {
					t.Errorf("stderr should contain also the indication that the diagnostics have been consolidated")
				}
			},
		},
		"diagnostics with sources": {

			diags: tfdiags.Diagnostics{}.
				Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Error with source",
					Detail:   "foo bar error",
					Subject:  hcl.RangeBetween(hcl.Range{Filename: "test.tf"}, hcl.Range{Filename: "test.tf"}).Ptr(),
				}).
				Append(&hcl.Diagnostic{
					Severity: hcl.DiagWarning,
					Summary:  "Warning with source",
					Detail:   "foo bar warning",
					Subject:  hcl.RangeBetween(hcl.Range{Filename: "test.tf"}, hcl.Range{Filename: "test.tf"}).Ptr(),
				}),
			setup: func(view *View) {
				testFile := &hcl.File{
					Bytes: []byte(`resource "null_resource" "test" {}`),
					Body:  hcl.EmptyBody(),
				}
				testSources := map[string]*hcl.File{
					"test.tf": testFile,
				}
				view.SetConfigSources(func() map[string]*hcl.File {
					return testSources
				})
			},
			validate: func(t *testing.T, output *terminal.TestOutput) {
				stderr := output.Stderr()
				expectedStderr := `
Error: Error with source

  on test.tf line 0:
   0: resource "null_resource" "test" {}

foo bar error
`
				if diff := cmp.Diff(expectedStderr, stderr); diff != "" {
					t.Fatalf("unexpected stderr:\n%s", diff)
				}
				stdout := output.Stdout()
				expectedStdout := `
Warning: Warning with source

  on test.tf line 0:
   0: resource "null_resource" "test" {}

foo bar warning
`
				if diff := cmp.Diff(expectedStdout, stdout); diff != "" {
					t.Fatalf("unexpected stdout:\n%s", diff)
				}
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			streams, done := terminal.StreamsForTesting(t)
			view := NewView(streams)

			if tc.setup != nil {
				tc.setup(view)
			}

			view.Diagnostics(tc.diags)
			output := done(t)
			tc.validate(t, output)
		})
	}
}

func TestView_HelpPrompt(t *testing.T) {
	for _, cmd := range []string{"apply", "init", "dummy"} {
		t.Run(cmd, func(t *testing.T) {
			streams, done := terminal.StreamsForTesting(t)
			view := NewView(streams)
			view.HelpPrompt(cmd)
			output := done(t)

			stderr := output.Stderr()
			if !strings.Contains(stderr, fmt.Sprintf("tofu %s -help", cmd)) {
				t.Errorf("expected help prompt to contain 'tofu apply -help', got %q", stderr)
			}
		})
	}
}

// TestViewColorize was moved from meta_test and renamed from TestMetaColorize.
// This was moved as is to keep the same testing patterns but in the context of the
// views package.
func TestViewColorize(t *testing.T) {
	t.Run("with color enabled", func(t *testing.T) {
		view, done := testView(t)
		defer done(t)

		args := []string{"foo", "bar"}
		wantArgs := []string{"foo", "bar"}
		viewArgs, args := arguments.ParseView(args)

		view.Configure(viewArgs)

		if !reflect.DeepEqual(args, wantArgs) {
			t.Fatalf("bad: %#v", args)
		}
		if view.Colorize().Disable {
			t.Fatal("should not be disabled")
		}
	})

	t.Run("one occurrence of -no-color flag", func(t *testing.T) {
		view, done := testView(t)
		defer done(t)

		args := []string{"foo", "-no-color", "bar"}
		args2 := []string{"foo", "bar"}
		viewArgs, args := arguments.ParseView(args)

		view.Configure(viewArgs)
		if !reflect.DeepEqual(args, args2) {
			t.Fatalf("bad: %#v", args)
		}
		if !view.Colorize().Disable {
			t.Fatal("should be disabled")
		}
	})

	t.Run("one occurrences of -no-color flag", func(t *testing.T) {
		view, done := testView(t)
		defer done(t)
		// Test disable #2
		// Verify multiple -no-color options are removed from args slice.
		// E.g. an additional -no-color arg could be added by TF_CLI_ARGS.
		args := []string{"foo", "-no-color", "bar", "-no-color"}
		args2 := []string{"foo", "bar"}
		viewArgs, args := arguments.ParseView(args)

		view.Configure(viewArgs)

		if !reflect.DeepEqual(args, args2) {
			t.Fatalf("bad: %#v", args)
		}
		if !view.Colorize().Disable {
			t.Fatal("should be disabled")
		}
	})
}
