package ui

import (
	"strings"
	"testing"
)

var testProviders = []string{"together", "claude", "openai", "gemini", "ollama", "lmstudio", "orcarouter"}

// A key-based provider with no key configured must expose a focusable API-key
// field, and navigation from the provider row must land on it.
func TestKeyFieldShownWhenProviderNeedsKey(t *testing.T) {
	m := newLaunchModel(testProviders, LaunchConfig{Provider: "together", KeyConfigured: false})
	if !m.keyFieldActive() {
		t.Fatal("together + no key: expected key field active")
	}
	view := stripANSI(m.View())
	if !strings.Contains(view, "API key") {
		t.Fatalf("together + no key: expected an 'API key' row, got:\n%s", view)
	}
	m.focus = fProvider
	m.move(1)
	if m.focus != fAPIKey {
		t.Fatalf("expected focus to land on fAPIKey, got %d", m.focus)
	}
}

// A local provider needs no key: the field is hidden and navigation skips it.
func TestKeyFieldHiddenForLocalProvider(t *testing.T) {
	m := newLaunchModel(testProviders, LaunchConfig{Provider: "ollama", KeyConfigured: false})
	if m.keyFieldActive() {
		t.Fatal("ollama: expected key field inactive")
	}
	m.focus = fProvider
	m.move(1)
	if m.focus != fSwarm {
		t.Fatalf("ollama: expected navigation to skip fAPIKey and land on fSwarm, got %d", m.focus)
	}
}

// When a key is already configured, a key-based provider must not prompt.
func TestKeyFieldHiddenWhenConfigured(t *testing.T) {
	m := newLaunchModel(testProviders, LaunchConfig{Provider: "claude", KeyConfigured: true})
	if m.keyFieldActive() {
		t.Fatal("claude + configured key: expected key field inactive")
	}
}

// RunLauncher only returns a pasted key for key-based providers.
func TestResultKeyOnlyForKeyProviders(t *testing.T) {
	if !providerNeedsKeyUI("together") {
		t.Fatal("together should need a key")
	}
	if providerNeedsKeyUI("ollama") {
		t.Fatal("ollama should not need a key")
	}
}

func stripANSI(s string) string {
	var b strings.Builder
	inEsc := false
	for _, r := range s {
		switch {
		case r == 0x1b:
			inEsc = true
		case inEsc && (r == 'm'):
			inEsc = false
		case inEsc:
			// swallow escape body
		default:
			b.WriteRune(r)
		}
	}
	return b.String()
}

func TestLauncher_LabPickerUsesSuppliedRegistry(t *testing.T) {
	def := LaunchConfig{Labs: []LabChoice{
		{Name: "dvwa", Display: "DVWA (Damn Vulnerable Web Application)"},
		{Name: "webgoat", Display: "OWASP WebGoat"},
	}}
	m := newLaunchModel(nil, def)
	disp := m.labDisplays()
	if len(disp) != 2 || disp[0] != "DVWA (Damn Vulnerable Web Application)" {
		t.Fatalf("picker should render supplied displays, got %v", disp)
	}
	if m.labName() != "dvwa" {
		t.Errorf("labName()=%q, want dvwa", m.labName())
	}
	m.labIdx = 1
	if m.labName() != "webgoat" {
		t.Errorf("labName() after cycle=%q, want webgoat", m.labName())
	}
}

func TestLauncher_LabPickerFallbackWhenEmpty(t *testing.T) {
	m := newLaunchModel(nil, LaunchConfig{}) // no Labs supplied
	if len(m.labChoices) == 0 {
		t.Fatal("empty Labs should fall back to a built-in default, not an empty picker")
	}
	if m.labName() == "" {
		t.Error("fallback should yield a selectable lab name")
	}
}

// The offline-demo target type appears only when demo scenarios are supplied,
// it needs no API key, and its picker renders the chosen target.
func TestDemoTargetTypeAndNoKey(t *testing.T) {
	demos := []LabChoice{{Name: "crapi", Display: "OWASP crAPI"}, {Name: "vampi", Display: "VAmPI"}}
	m := newLaunchModel(testProviders, LaunchConfig{Provider: "together", KeyConfigured: false, Demos: demos})

	// Three target types now (custom URL | bundled lab | offline demo).
	if got := len(m.targetTypes()); got != 3 {
		t.Fatalf("targetTypes = %d, want 3", got)
	}
	// Select the demo type.
	m.focus = fTargetType
	m.adjust(1) // -> bundled lab
	m.adjust(1) // -> offline demo
	if !m.demoSelected() {
		t.Fatalf("expected demoSelected after two toggles, tType=%d", m.tType)
	}
	// A demo needs no key even on a key-based provider.
	if m.keyFieldActive() {
		t.Fatal("demo: expected key field inactive")
	}
	// Picker cycles the demo targets.
	m.focus = fTargetOrLab
	if m.demoName() != "crapi" {
		t.Fatalf("demoName = %q, want crapi", m.demoName())
	}
	m.adjust(1)
	if m.demoName() != "vampi" {
		t.Fatalf("after adjust, demoName = %q, want vampi", m.demoName())
	}
	if view := stripANSI(m.View()); !strings.Contains(view, "VAmPI") || !strings.Contains(view, "offline replay") {
		t.Fatalf("demo picker not rendered:\n%s", view)
	}
}

// With no demo scenarios supplied, the target type stays a 2-way toggle.
func TestNoDemosKeepsTwoWayToggle(t *testing.T) {
	m := newLaunchModel(testProviders, LaunchConfig{Provider: "together"})
	if got := len(m.targetTypes()); got != 2 {
		t.Fatalf("targetTypes = %d, want 2 when no demos", got)
	}
	m.focus = fTargetType
	m.adjust(1)
	m.adjust(1) // two toggles should wrap back to 0, never reach demo
	if m.demoSelected() {
		t.Fatal("demoSelected should be false with no demo scenarios")
	}
}

// The Context field is present, focusable/editable, and its value is carried
// out; the target row advertises multiple targets.
func TestContextFieldAndMultiTargetLabel(t *testing.T) {
	m := newLaunchModel(testProviders, LaunchConfig{Provider: "together", KeyConfigured: true})
	view := stripANSI(m.View())
	if !strings.Contains(view, "Context") {
		t.Fatalf("expected a Context row, got:\n%s", view)
	}
	if !strings.Contains(view, "Target(s)") {
		t.Errorf("expected the target row to read 'Target(s)' (multi-target), got:\n%s", view)
	}
	// Navigate to the Context field and confirm it's an editable text field.
	m.focus = fContext
	m.refocus()
	if !m.editingText() {
		t.Fatal("Context field should be editable text")
	}
	m.tiCtx.SetValue("~/engagements/acme/context")
	cfg := LaunchConfig{
		Context: strings.TrimSpace(m.tiCtx.Value()),
	}
	if cfg.Context != "~/engagements/acme/context" {
		t.Errorf("Context not carried: %q", cfg.Context)
	}
}
