package terminal

import (
	"testing"

	"github.com/jandedobbeleer/oh-my-posh/src/color"
	"github.com/jandedobbeleer/oh-my-posh/src/shell"

	"github.com/stretchr/testify/assert"
)

func TestWriteANSIColors(t *testing.T) {
	cases := []struct {
		Case               string
		Expected           string
		Input              string
		Colors             *color.Set
		Parent             *color.Set
		TerminalBackground color.Ansi
	}{
		{
			Case:     "Inline override identical",
			Input:    "\ue0a0saturnay <red>↑</>1",
			Expected: "\x1b[31m\ue0a0saturnay ↑1\x1b[0m",
			Colors:   &color.Set{Foreground: "red", Background: color.Transparent},
		},
		{
			Case:     "Bold",
			Input:    "<b>test</b>",
			Expected: "\x1b[1m\x1b[30mtest\x1b[22m\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold with color override",
			Input:    "<b><#ffffff>test</></b>",
			Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[30m\x1b[22m\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold with color override, flavor 2",
			Input:    "<#ffffff><b>test</b></>",
			Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Double override",
			Input:    "<#ffffff>jan</>@<#ffffff>Jans-MBP</>",
			Expected: "\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mjan\x1b[32m@\x1b[38;2;255;255;255mJans-MBP\x1b[0m",
			Colors:   &color.Set{Foreground: "green", Background: "#FF5733"},
		},
		{
			Case:     "No color override",
			Input:    "test",
			Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
			Parent:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Inherit foreground",
			Input:    "test",
			Expected: "\x1b[47m\x1b[33mtest\x1b[0m",
			Colors:   &color.Set{Foreground: color.ParentForeground, Background: "white"},
			Parent:   &color.Set{Foreground: "yellow", Background: "white"},
		},
		{
			Case:     "Inherit background",
			Input:    "test",
			Expected: "\x1b[41m\x1b[30mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
			Parent:   &color.Set{Foreground: "yellow", Background: "red"},
		},
		{
			Case:     "No parent",
			Input:    "test",
			Expected: "\x1b[30mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Inherit override foreground",
			Input:    "hello <parentForeground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[33mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
			Parent:   &color.Set{Foreground: "yellow", Background: "red"},
		},
		{
			Case:     "Inherit override background",
			Input:    "hello <black,parentBackground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
			Parent:   &color.Set{Foreground: "yellow", Background: "red"},
		},
		{
			Case:     "Inherit override background, no foreground specified",
			Input:    "hello <,parentBackground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
			Parent:   &color.Set{Foreground: "yellow", Background: "red"},
		},
		{
			Case:     "Inherit no parent foreground",
			Input:    "hello <parentForeground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[37;49m\x1b[7mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Inherit no parent background",
			Input:    "hello <,parentBackground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[49mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Inherit override both",
			Input:    "hello <parentForeground,parentBackground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[41m\x1b[33mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
			Parent:   &color.Set{Foreground: "yellow", Background: "red"},
		},
		{
			Case:     "Inherit override both inverted",
			Input:    "hello <parentBackground,parentForeground>world</>",
			Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
			Parent:   &color.Set{Foreground: "yellow", Background: "red"},
		},
		{
			Case:     "Inline override",
			Input:    "hello, <red>world</>, rabbit",
			Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld\x1b[30m, rabbit\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "color.Transparent background",
			Input:    "hello world",
			Expected: "\x1b[37mhello world\x1b[0m",
			Colors:   &color.Set{Foreground: "white", Background: color.Transparent},
		},
		{
			Case:     "color.Transparent foreground override",
			Input:    "hello <#ffffff>world</>",
			Expected: "\x1b[32mhello \x1b[38;2;255;255;255mworld\x1b[0m",
			Colors:   &color.Set{Foreground: "green", Background: color.Transparent},
		},
		{
			Case:     "No foreground",
			Input:    "test",
			Expected: "\x1b[48;2;255;87;51m\x1b[37mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "", Background: "#FF5733"},
		},
		{
			Case:     "color.Transparent foreground",
			Input:    "test",
			Expected: "\x1b[0m\x1b[38;2;255;87;51;49m\x1b[7mtest\x1b[0m",
			Colors:   &color.Set{Foreground: color.Transparent, Background: "#FF5733"},
		},
		{
			Case:               "color.Transparent foreground, terminal background set",
			Input:              "test",
			Expected:           "\x1b[38;2;33;47;60m\x1b[48;2;255;87;51mtest\x1b[0m",
			Colors:             &color.Set{Foreground: color.Transparent, Background: "#FF5733"},
			TerminalBackground: "#212F3C",
		},
		{
			Case:     "Foreground for foreground override",
			Input:    "<foreground>test</>",
			Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Background for background override",
			Input:    "<,background>test</>",
			Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Google",
			Input:    "<blue,white>G</><red,white>o</><yellow,white>o</><blue,white>g</><green,white>l</><red,white>e</>",
			Expected: "\x1b[47m\x1b[34mG\x1b[40m\x1b[30m\x1b[47m\x1b[31mo\x1b[40m\x1b[30m\x1b[47m\x1b[33mo\x1b[40m\x1b[30m\x1b[47m\x1b[34mg\x1b[40m\x1b[30m\x1b[47m\x1b[32ml\x1b[40m\x1b[30m\x1b[47m\x1b[31me\x1b[0m", //nolint: lll
			Colors:   &color.Set{Foreground: "black", Background: "black"},
		},
		{
			Case:     "Foreground for background override",
			Input:    "<background>test</>",
			Expected: "\x1b[47m\x1b[37mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Foreground for background vice versa override",
			Input:    "<background,foreground>test</>",
			Expected: "\x1b[40m\x1b[37mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Background for foreground override",
			Input:    "<,foreground>test</>",
			Expected: "\x1b[40m\x1b[30mtest\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Nested override",
			Input:    "hello, <red>world, <white>rabbit</> hello</>",
			Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld, \x1b[37mrabbit\x1b[31m hello\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "color.Transparent override",
			Input:    "home<transparent> / </>code<transparent> / </>src ",
			Expected: "\x1b[47m\x1b[30mhome\x1b[0m\x1b[37;49m\x1b[7m / \x1b[27m\x1b[47m\x1b[30mcode\x1b[0m\x1b[37;49m\x1b[7m / \x1b[27m\x1b[47m\x1b[30msrc \x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "Larger than and less than symbols",
			Input:    "<red><</><orange>></><yellow><</>",
			Expected: "\x1b[47m\x1b[31m<\x1b[30m>\x1b[33m<\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "color.Transparent override with parent",
			Input:    "hello <#011627,#82AAFF>new</> world",
			Expected: "\x1b[33mhello \x1b[48;2;130;170;255m\x1b[38;2;1;22;39mnew\x1b[49m\x1b[33m world\x1b[0m",
			Colors:   &color.Set{Foreground: "yellow", Background: "transparent"},
		},
		{
			Case:     "OSC injection stripped from segment content",
			Input:    "before\x1b]0;HACKED\x07after",
			Expected: "\x1b[47m\x1b[30mbefore]0;HACKEDafter\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
		{
			Case:     "CSI and C1 control runes stripped from segment content",
			Input:    "before\x1b[31mred\u009bafter",
			Expected: "\x1b[47m\x1b[30mbefore[31mredafter\x1b[0m",
			Colors:   &color.Set{Foreground: "black", Background: "white"},
		},
	}

	for _, tc := range cases {
		Init(shell.GENERIC)
		ParentColors = []*color.Set{tc.Parent}
		CurrentColors = tc.Colors
		BackgroundColor = tc.TerminalBackground
		Colors = &color.Defaults{}

		Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)

		got, _ := String()

		assert.Equal(t, tc.Expected, got, tc.Case)
	}
}

func TestWriteLength(t *testing.T) {
	cases := []struct {
		Colors   *color.Set
		Case     string
		Input    string
		Expected int
	}{
		{
			Case:     "Emoji",
			Input:    " ⏰  ",
			Expected: 5,
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold",
			Input:    "<b>test</b>",
			Expected: 4,
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold with color override",
			Input:    "<b><#ffffff>test</></b>",
			Expected: 4,
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold with color override and link",
			Input:    "<b><#ffffff>test</></b> <LINK>https://example.com<TEXT>url</TEXT></LINK>",
			Expected: 8,
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold with color override and link and leading/trailing spaces",
			Input:    " <b><#ffffff>test</></b> <LINK>https://example.com<TEXT>url</TEXT></LINK> ",
			Expected: 10,
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
		{
			Case:     "Bold with color override and link without text and leading/trailing spaces",
			Input:    " <b><#ffffff>test</></b> <LINK>https://example.com<TEXT></TEXT></LINK> ",
			Expected: 11,
			Colors:   &color.Set{Foreground: "black", Background: color.ParentBackground},
		},
	}

	for _, tc := range cases {
		Init(shell.GENERIC)
		ParentColors = []*color.Set{}
		CurrentColors = tc.Colors
		Colors = &color.Defaults{}

		Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)

		_, got := String()

		assert.Equal(t, tc.Expected, got, tc.Case)
	}
}

func TestProgressFunctions(t *testing.T) {
	originalProgram := Program
	originalProgressTerminals := progressTerminals

	t.Cleanup(func() {
		Program = originalProgram
		progressTerminals = originalProgressTerminals
	})

	cases := []struct {
		Features       *Features
		Case           string
		Program        string
		ExpectProgress bool
	}{
		{
			Case:           "Windows Terminal default",
			Program:        WindowsTerminal,
			ExpectProgress: true,
		},
		{
			Case:    "Unknown terminal default",
			Program: Unknown,
		},
		{
			Case:           "Ghostty configured",
			Program:        "ghostty",
			Features:       &Features{Progress: []string{"ghostty", WindowsTerminal}},
			ExpectProgress: true,
		},
		{
			Case:           "case insensitive match",
			Program:        "ghostty",
			Features:       &Features{Progress: []string{"Ghostty"}},
			ExpectProgress: true,
		},
		{
			Case:           "Windows Terminal with custom list",
			Program:        WindowsTerminal,
			Features:       &Features{Progress: []string{"ghostty", WindowsTerminal}},
			ExpectProgress: true,
		},
		{
			Case:     "Unknown terminal with custom list",
			Program:  Unknown,
			Features: &Features{Progress: []string{"ghostty", WindowsTerminal}},
		},
		{
			Case:     "Custom terminal not in list",
			Program:  "alacritty",
			Features: &Features{Progress: []string{"ghostty"}},
		},
		{
			Case:           "empty features keep the default",
			Program:        WindowsTerminal,
			Features:       &Features{},
			ExpectProgress: true,
		},
		{
			Case:           "empty list keeps the default",
			Program:        WindowsTerminal,
			Features:       &Features{Progress: []string{}},
			ExpectProgress: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Case, func(t *testing.T) {
			Program = tc.Program
			progressTerminals = []string{WindowsTerminal}

			tc.Features.Apply()

			if tc.ExpectProgress {
				assert.NotEmpty(t, StartProgress(), tc.Case)
				assert.NotEmpty(t, SetProgress(50), tc.Case)
				assert.NotEmpty(t, StopProgress(), tc.Case)
				return
			}

			assert.Empty(t, StartProgress(), tc.Case)
			assert.Empty(t, SetProgress(50), tc.Case)
			assert.Empty(t, StopProgress(), tc.Case)
		})
	}
}

// paletteTestColors is a minimal color.String stand-in for TestAsAnsiColorsWithSourceSurvivesResolution's
// palette case: it defers ToAnsi to an embedded *color.Defaults and only overrides Resolve to run
// palette lookup, mirroring color.PaletteColors.Resolve without needing its unexported palette field.
type paletteTestColors struct {
	*color.Defaults
	palette color.Palette
}

func (p *paletteTestColors) Resolve(colorString color.Ansi) (color.Ansi, error) {
	return p.palette.ResolveColor(colorString)
}

// TestAsAnsiColorsWithSourceSurvivesResolution proves the resolve step (resolveAnsiColors,
// exercised here through asAnsiColorsWithSource) hands back each channel's SOURCE form
// unmangled, for every shape it can take. This is the value asAnsiColors used to discard
// before ToAnsi's SGR conversion (writer.go's asAnsiColors historically returned only the
// SGR pair); Stage 1 of the runs-as-intermediate-representation plan makes it available
// alongside the SGR pair on the same color.History entry (see color.History.Add) because
// SGR cannot be inverted back to it reliably.
func TestAsAnsiColorsWithSourceSurvivesResolution(t *testing.T) {
	defer func() {
		Colors = nil
		CurrentColors = nil
		ParentColors = nil
	}()

	cases := []struct {
		Case       string
		Input      color.Ansi
		Colors     color.String
		WantSource color.Ansi
	}{
		{
			Case:       "#RRGGBB hex",
			Input:      "#FF5733",
			Colors:     &color.Defaults{},
			WantSource: "#FF5733",
		},
		{
			Case:       "colour name from ansiColorCodes",
			Input:      "red",
			Colors:     &color.Defaults{},
			WantSource: "red",
		},
		{
			Case:       "transparent",
			Input:      color.Transparent,
			Colors:     &color.Defaults{},
			WantSource: color.Transparent,
		},
		{
			Case:       "accent",
			Input:      color.Accent,
			Colors:     &color.Defaults{},
			WantSource: color.Accent,
		},
		{
			Case:  "palette reference p:name",
			Input: "p:myColor",
			Colors: &paletteTestColors{
				Defaults: &color.Defaults{},
				palette:  color.Palette{"myColor": "#123456"},
			},
			WantSource: "#123456",
		},
		{
			Case:       "plain numeric 0-255",
			Input:      "196",
			Colors:     &color.Defaults{},
			WantSource: "196",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Case, func(t *testing.T) {
			Colors = tc.Colors
			CurrentColors = nil
			ParentColors = nil

			// background is under test; foreground is a fixed, uninteresting "white"
			// so inverted never triggers and doesn't confound the background assertions.
			bg, _, bgSource, _ := asAnsiColorsWithSource(tc.Input, "white")

			assert.Equal(t, tc.WantSource, bgSource, "source form must survive resolution unmodified")

			// the returned SGR must be exactly ToAnsi(source): the two representations
			// must never diverge, or a later encoder reading source could not trust it
			// describes the same color the SGR pair already printed.
			assert.Equal(t, tc.Colors.ToAnsi(bgSource, true), bg, "SGR must equal ToAnsi(source)")

			// SGR is a genuinely different (and, for hex/numeric, lossy) encoding of the
			// same value, except where ToAnsi is an identity function (transparent).
			if tc.WantSource != color.Transparent {
				assert.NotEqual(t, string(bgSource), string(bg), "SGR and source must differ")
			}
		})
	}
}

// maliciousPwd embeds ESC-ST (which closes an OSC sequence early) followed by
// an independent OSC 0 title-set: if Pwd ever stops sanitizing, this reaches
// the terminal as two escape sequences instead of one opaque payload.
const maliciousPwd = "evil\x1b\\\x1b]0;PWNED\x07rest"

func TestPwd(t *testing.T) {
	cases := []struct {
		Case     string
		PwdType  string
		Pwd      string
		UserName string
		HostName string
		Expected string
	}{
		{
			Case:     "OSC7 clean pwd",
			PwdType:  OSC7,
			Pwd:      "/home/user/project",
			HostName: "box",
			Expected: "\x1b]7;file://box//home/user/project\x1b\\",
		},
		{
			Case:     "OSC7 malicious pwd is sanitized",
			PwdType:  OSC7,
			Pwd:      maliciousPwd,
			HostName: "box",
			Expected: "\x1b]7;file://box/evil\\]0;PWNEDrest\x1b\\",
		},
		{
			Case:     "OSC51 clean pwd",
			PwdType:  OSC51,
			Pwd:      "/home/user/project",
			UserName: "jan",
			HostName: "box",
			Expected: "\x1b]51;Ajan@box:/home/user/project\x1b\\",
		},
		{
			Case:     "OSC51 malicious pwd, user and host are sanitized",
			PwdType:  OSC51,
			Pwd:      maliciousPwd,
			UserName: maliciousPwd,
			HostName: maliciousPwd,
			Expected: "\x1b]51;Aevil\\]0;PWNEDrest@evil\\]0;PWNEDrest:evil\\]0;PWNEDrest\x1b\\",
		},
		{
			Case:     "OSC99 clean pwd",
			PwdType:  OSC99,
			Pwd:      "/home/user/project",
			Expected: "\x1b]9;9;/home/user/project\x1b\\",
		},
		{
			Case:     "OSC99 malicious pwd is sanitized",
			PwdType:  OSC99,
			Pwd:      maliciousPwd,
			Expected: "\x1b]9;9;evil\\]0;PWNEDrest\x1b\\",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Case, func(t *testing.T) {
			Init(shell.GENERIC)

			got := Pwd(tc.PwdType, tc.UserName, tc.HostName, tc.Pwd)

			assert.Equal(t, tc.Expected, got, tc.Case)
			assert.NotContains(t, got, "\x1b]0;PWNED\x07", tc.Case)
		})
	}
}

func TestSetCursorStyle(t *testing.T) {
	cases := []struct {
		Case     string
		Shell    string
		Style    string
		Expected string
		Plain    bool
	}{
		{
			Case:     "Steady block, generic shell",
			Shell:    shell.GENERIC,
			Style:    SteadyBlock,
			Expected: "\x1b[2 q",
		},
		{
			Case:     "Blinking bar, generic shell",
			Shell:    shell.GENERIC,
			Style:    BlinkingBar,
			Expected: "\x1b[5 q",
		},
		{
			Case:     "Steady underline wrapped for bash readline",
			Shell:    shell.BASH,
			Style:    SteadyUnderline,
			Expected: "\\[\x1b[4 q\\]",
		},
		{
			Case:     "Steady bar wrapped for zsh",
			Shell:    shell.ZSH,
			Style:    SteadyBar,
			Expected: "%{\x1b[6 q%}",
		},
		{
			Case:     "Unknown style renders nothing",
			Shell:    shell.GENERIC,
			Style:    "not-a-style",
			Expected: "",
		},
		{
			Case:     "Default steady, generic shell",
			Shell:    shell.GENERIC,
			Style:    DefaultSteady,
			Expected: "\x1b[0 q\x1b[?12l",
		},
		{
			Case:     "Default blinking wrapped for zsh",
			Shell:    shell.ZSH,
			Style:    DefaultBlinking,
			Expected: "%{\x1b[0 q\x1b[?12h%}",
		},
		{
			Case:     "Plain mode renders nothing",
			Shell:    shell.GENERIC,
			Style:    SteadyBlock,
			Plain:    true,
			Expected: "",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Case, func(t *testing.T) {
			Init(tc.Shell)
			Plain = tc.Plain

			got := SetCursorStyle(tc.Style)

			assert.Equal(t, tc.Expected, got, tc.Case)
		})
	}

	Plain = false
}

func TestStripControlRunes(t *testing.T) {
	cases := []struct {
		Case     string
		Input    string
		Expected string
	}{
		{
			Case:     "empty string",
			Input:    "",
			Expected: "",
		},
		{
			Case:     "printable unicode is untouched",
			Input:    "jan @ 世界 café",
			Expected: "jan @ 世界 café",
		},
		{
			Case:     "C0 and C1 control runes removed",
			Input:    "evil\x1b\\\x1b]0;PWNED\x07rest",
			Expected: "evil\\]0;PWNEDrest",
		},
		{
			Case:     "newline removed, unlike isControlRune",
			Input:    "line1\nline2",
			Expected: "line1line2",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Case, func(t *testing.T) {
			got := stripControlRunes(tc.Input)

			assert.Equal(t, tc.Expected, got, tc.Case)
		})
	}
}
