package prompt

import (
	"testing"
	"time"

	"github.com/jandedobbeleer/oh-my-posh/src/cache"
	"github.com/jandedobbeleer/oh-my-posh/src/color"
	"github.com/jandedobbeleer/oh-my-posh/src/config"
	"github.com/jandedobbeleer/oh-my-posh/src/maps"
	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
	"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
	"github.com/jandedobbeleer/oh-my-posh/src/shell"
	"github.com/jandedobbeleer/oh-my-posh/src/template"

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

func TestRenderBlock(t *testing.T) {
	engine := New(&runtime.Flags{
		IsPrimary: true,
	})
	block := &config.Block{
		Segments: []*config.Segment{
			{
				Type:       "text",
				Template:   "Hello",
				Foreground: "red",
				Background: "blue",
			},
			{
				Type:       "text",
				Template:   "World",
				Foreground: "red",
				Background: "blue",
			},
		},
	}

	prompt, length := engine.writeBlockSegments(block)
	assert.Equal(t, "\x1b[44m\x1b[31mHello\x1b[0m\x1b[44m\x1b[31mWorld\x1b[0m", prompt)
	assert.Equal(t, 10, length)
}

func TestRenderBlockRestartCycle(t *testing.T) {
	buildCycle := func() color.Cycle {
		return color.Cycle{
			{Foreground: "#000001", Background: "#000011"},
			{Foreground: "#000002", Background: "#000022"},
			{Foreground: "#000003", Background: "#000033"},
		}
	}

	cases := []struct {
		Case               string
		ExpectedForeground color.Ansi
		ExpectedBackground color.Ansi
		RestartCycle       bool
	}{
		{
			Case:               "No restart carries the cycle over from the previous block",
			RestartCycle:       false,
			ExpectedForeground: "#000003",
			ExpectedBackground: "#000033",
		},
		{
			Case:               "RestartCycle resets the block to the cycle's first color",
			RestartCycle:       true,
			ExpectedForeground: "#000001",
			ExpectedBackground: "#000011",
		},
	}

	for _, c := range cases {
		t.Run(c.Case, func(t *testing.T) {
			origCycle := cycle
			t.Cleanup(func() { cycle = origCycle })

			engine := New(&runtime.Flags{
				IsPrimary: true,
			})
			engine.Config.Cycle = buildCycle()

			// mirror writePrimaryPromptInternal, which points the shared
			// cycle at the pristine config cycle before the first block renders
			cycle = &engine.Config.Cycle

			firstBlock := &config.Block{
				Segments: []*config.Segment{
					{Type: "text", Template: "A"},
					{Type: "text", Template: "B"},
				},
			}
			secondBlock := &config.Block{
				RestartCycle: c.RestartCycle,
				Segments: []*config.Segment{
					{Type: "text", Template: "C"},
				},
			}

			engine.writeBlockSegments(firstBlock)
			engine.writeBlockSegments(secondBlock)

			assert.Equal(t, c.ExpectedForeground, secondBlock.Segments[0].Foreground, c.Case)
			assert.Equal(t, c.ExpectedBackground, secondBlock.Segments[0].Background, c.Case)
		})
	}
}

func TestCanRenderSegment(t *testing.T) {
	cases := []struct {
		Case     string
		Executed map[string]bool
		Needs    []string
		Expected bool
	}{
		{
			Case:     "No cross segment dependencies",
			Expected: true,
		},
		{
			Case:     "Cross segment dependencies, nothing executed",
			Expected: false,
			Needs:    []string{"Foo"},
		},
		{
			Case:     "Cross segment dependencies, available",
			Expected: true,
			Executed: map[string]bool{
				"Foo": true,
			},
			Needs: []string{"Foo"},
		},
	}
	for _, c := range cases {
		segment := &config.Segment{
			Type:  "text",
			Needs: c.Needs,
		}

		engine := &Engine{}
		got := engine.canRenderSegment(segment, c.Executed)

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

func TestExecuteSegmentWithTimeout_Streaming(t *testing.T) {
	// This test verifies that when streaming is enabled and a timeout occurs,
	// the segment is marked as pending and tracked for later completion
	env := new(mock.Environment)
	env.On("Flags").Return(&runtime.Flags{Streaming: true})

	segment := &config.Segment{
		Type:    "text",
		Timeout: 1, // Very short timeout to ensure it triggers
	}

	engine := &Engine{
		Env:              env,
		streamingResults: make(chan *config.Segment, 10),
	}

	// Create a mock segment that will definitely timeout
	// We'll use the actual timeout mechanism by making the execution slow
	done := make(chan bool)
	go func() {
		time.Sleep(100 * time.Millisecond) // Longer than timeout
		close(done)
	}()

	// Pre-register segment as pending (this happens in writeSegmentsConcurrently)
	engine.pendingSegments.Store(segment.Name(), true)

	// Mark as pending and track (simulating what executeSegmentWithTimeout does)
	segment.Pending = true
	engine.trackPendingSegment(segment, done)

	// Verify it was tracked as pending
	_, exists := engine.pendingSegments.Load(segment.Name())
	assert.True(t, exists, "Segment should be tracked as pending")

	// Wait for completion notification
	select {
	case completed := <-engine.streamingResults:
		assert.Equal(t, segment, completed)
		assert.False(t, completed.Pending, "Segment should no longer be pending after completion")
	case <-time.After(200 * time.Millisecond):
		t.Error("Expected segment completion notification")
	}
}

func TestExecuteSegmentWithTimeout_NonStreaming(t *testing.T) {
	// This test verifies that when streaming is disabled,
	// trackPendingSegment returns early without tracking when streamingResults is nil
	segment := &config.Segment{
		Type:    "text",
		Timeout: 10,
	}

	engine := &Engine{
		// streamingResults is nil (non-streaming mode)
	}

	done := make(chan bool)

	// Pre-register segment (simulating what happens in concurrent execution)
	engine.pendingSegments.Store(segment.Name(), true)

	// trackPendingSegment should not track when streamingResults is nil
	engine.trackPendingSegment(segment, done)

	// Signal completion
	close(done)

	// Give time for any goroutine to run (shouldn't be one)
	time.Sleep(50 * time.Millisecond)

	// Segment should still be in pendingSegments because notifySegmentCompletion
	// was never called (trackPendingSegment returns early when streamingResults is nil)
	_, exists := engine.pendingSegments.Load(segment.Name())
	assert.True(t, exists, "Segment should remain in pendingSegments when streaming is disabled")
}

func TestExecuteSegmentWithTimeout_CachedValueFallback(t *testing.T) {
	// This test verifies that a pending segment's Text() returns "..." placeholder
	env := new(mock.Environment)
	env.On("Flags").Return(&runtime.Flags{})
	env.On("Shell").Return(shell.GENERIC)

	// Render writes through the template cache, so it has to exist rather than
	// be inherited from whichever test happened to run first.
	template.Cache = &cache.Template{
		Segments: maps.NewConcurrent[any](),
	}
	template.Init(env, nil, nil)

	segment := &config.Segment{
		Type:     "text",
		Pending:  true,
		Template: "actual content",
	}

	// Initialize the segment writer
	err := segment.MapSegmentWithWriter(env)
	assert.NoError(t, err)

	// Render with pending state - should show "..."
	segment.Render(0, true)
	text := segment.Text()
	assert.Equal(t, "...", text, "Pending segment should show ...")

	// After completion, render again with actual content
	segment.Pending = false
	segment.Render(0, true)
	text = segment.Text()
	assert.NotEqual(t, "...", text, "Non-pending segment should show actual content")
}
