package cli

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"reasonix/internal/acp"
	"reasonix/internal/config"
	"reasonix/internal/event"
	"reasonix/internal/netclient"
	"reasonix/internal/provider"
	"reasonix/internal/tool"

	_ "reasonix/internal/tool/builtin"
)

const acpTestProviderKind = "acp-test-provider"

func init() {
	provider.Register(acpTestProviderKind, func(cfg provider.Config) (provider.Provider, error) {
		return &acpTestProvider{cfg: cfg}, nil
	})
}

func TestACPBuiltinToolsKeepSessionLevelBuiltins(t *testing.T) {
	dir := t.TempDir()
	tools := toolMap(acpBuiltinTools(&config.Config{}, dir, []string{dir}))
	for _, name := range []string{
		"todo_write",
		"complete_step",
		"bash_output",
		"kill_shell",
		"wait",
		"move_file",
		"notebook_edit",
	} {
		if tools[name] == nil {
			t.Fatalf("ACP workspace tools missing %q; got %v", name, toolNames(tools))
		}
	}
}

func TestACPInitializesWithoutAPIKey(t *testing.T) {
	isolateCLIConfigHome(t)
	t.Setenv("DEEPSEEK_API_KEY", "")
	oldStdin := os.Stdin
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatal(err)
	}
	_, _ = w.WriteString(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}` + "\n")
	_ = w.Close()
	os.Stdin = r
	t.Cleanup(func() {
		os.Stdin = oldStdin
		_ = r.Close()
	})

	out := captureStdout(t, func() {
		if rc := Run([]string{"--acp"}, "test-version"); rc != 0 {
			t.Fatalf("Run --acp initialize rc = %d, want 0", rc)
		}
	})
	if !strings.Contains(out, `"protocolVersion":1`) || !strings.Contains(out, `"name":"reasonix"`) {
		t.Fatalf("initialize output = %s", out)
	}
}

func TestACPRejectsInvalidSupervisorFlags(t *testing.T) {
	for _, args := range [][]string{
		{"--planner=maybe"},
		{"--sandbox-network=maybe"},
		{"--sandbox-bash=maybe"},
	} {
		if rc := acpCommand(args, "test-version"); rc != 2 {
			t.Fatalf("acpCommand(%v) rc = %d, want 2", args, rc)
		}
	}
}

func TestACPSupervisorRuntimeStateUsesHardOverrides(t *testing.T) {
	isolateCLIConfigHome(t)
	project := t.TempDir()
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
[agent]
planner_model = "configured-planner"

[sandbox]
network = true
bash = "off"

allow_write = ["../outside"]
`), 0o644); err != nil {
		t.Fatal(err)
	}
	off := false
	factory := &acpFactory{
		plannerOff: true, networkOverride: &off, bashOverride: "enforce", workspaceOnly: true,
	}
	state, err := factory.SessionRuntimeState(context.Background(), acp.SessionRuntimeStateParams{
		Cwd: project, Model: "configured-planner", RuntimeProfile: "balanced",
	})
	if err != nil {
		t.Fatalf("SessionRuntimeState: %v", err)
	}
	if state.PlannerMode != "off" || state.Sandbox.Mode != "enforce" || state.Sandbox.NetworkEnabled {
		t.Fatalf("runtime overrides = %+v", state)
	}
	if len(state.Sandbox.WriteRoots) != 1 || state.Sandbox.WriteRoots[0] != project || state.Sandbox.WorkspaceRoot != project {
		t.Fatalf("workspace confinement = %+v", state.Sandbox)
	}
}

func TestACPSupervisorRuntimeStateDegradesWhenSandboxIsUnavailable(t *testing.T) {
	isolateCLIConfigHome(t)
	project := t.TempDir()
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte("[sandbox]\nbash = \"enforce\"\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	unavailable := func() bool { return false }
	params := acp.SessionRuntimeStateParams{Cwd: project, RuntimeProfile: "balanced"}

	state, err := (&acpFactory{
		bashOverride: "enforce", sandboxAvailable: unavailable,
	}).SessionRuntimeState(context.Background(), params)
	if err != nil {
		t.Fatalf("default ACP startup must report unavailable sandbox instead of failing: %v", err)
	}
	if state.Sandbox.Mode != "enforce" || state.Sandbox.Available {
		t.Fatalf("degraded sandbox state = %+v", state.Sandbox)
	}

	_, err = (&acpFactory{
		bashOverride: "enforce", requireSandbox: true, sandboxAvailable: unavailable,
	}).SessionRuntimeState(context.Background(), params)
	if err == nil || !strings.Contains(err.Error(), "sandbox unavailable") {
		t.Fatalf("explicit enforce must fail closed, got %v", err)
	}
}

func TestEffectiveACPPlannerModeIsIndependentOfExecutionMode(t *testing.T) {
	cfg := config.Default()
	cfg.Agent.PlannerModel = "planner/planner-model"
	cfg.Providers = []config.ProviderEntry{
		{Name: "executor", Kind: "openai", Model: "executor-model"},
		{Name: "planner", Kind: "openai", Model: "planner-model"},
	}
	if got := effectiveACPPlannerMode(cfg, false, "executor/executor-model"); got != "on" {
		t.Fatalf("split-model planner mode = %q, want on", got)
	}
	if got := effectiveACPPlannerMode(cfg, true, "executor/executor-model"); got != "off" {
		t.Fatalf("disabled planner mode = %q, want off", got)
	}
	if got := effectiveACPPlannerMode(cfg, false, "planner/planner-model"); got != "off" {
		t.Fatalf("same-model planner mode = %q, want off", got)
	}
	cfg.Agent.PlannerModel = ""
	if got := effectiveACPPlannerMode(cfg, false, "executor/executor-model"); got != "off" {
		t.Fatalf("missing planner model = %q, want off", got)
	}
}

func TestACPFactoryLoadsSessionCwdProjectConfig(t *testing.T) {
	home := isolateCLIConfigHome(t)
	if _, err := config.SetCredential("REASONIX_TEST_KEY", "test-key"); err != nil {
		t.Fatalf("SetCredential: %v", err)
	}
	project := t.TempDir()
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
default_model = "local"

[[providers]]
name = "local"
kind = "acp-test-provider"
base_url = "http://example.invalid"
model = "fake-model"
api_key_env = "REASONIX_TEST_KEY"
`), 0o644); err != nil {
		t.Fatal(err)
	}
	cmdDir := filepath.Join(project, ".reasonix", "commands")
	if err := os.MkdirAll(cmdDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(cmdDir, "acp-only.md"), []byte("ACP project command"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.Chdir(home); err != nil {
		t.Fatal(err)
	}

	ctrl, err := (&acpFactory{}).NewSession(context.Background(), acp.SessionParams{Cwd: project, Sink: event.Discard})
	if err != nil {
		t.Fatalf("NewSession: %v", err)
	}
	defer ctrl.Close()

	for _, cmd := range ctrl.Commands() {
		if cmd.Name == "acp-only" {
			return
		}
	}
	t.Fatalf("ACP session did not load project command from cwd; commands=%v", ctrl.Commands())
}

func TestACPFactoryClearsEffortOverrideForUnsupportedModel(t *testing.T) {
	isolateCLIConfigHome(t)
	if _, err := config.SetCredential("REASONIX_TEST_KEY", "test-key"); err != nil {
		t.Fatalf("SetCredential: %v", err)
	}
	project := t.TempDir()
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
default_model = "reasoner/reasoning-model"

[[providers]]
name = "reasoner"
kind = "acp-test-provider"
base_url = "http://example.invalid"
model = "reasoning-model"
api_key_env = "REASONIX_TEST_KEY"
supported_efforts = ["low", "high"]

[[providers]]
name = "plain"
kind = "acp-test-provider"
base_url = "http://example.invalid"
model = "plain-model"
api_key_env = "REASONIX_TEST_KEY"
effort = "high"
`), 0o644); err != nil {
		t.Fatal(err)
	}

	high := "high"
	state, err := (&acpFactory{}).SessionConfigState(context.Background(), acp.SessionConfigStateParams{
		Cwd:            project,
		Model:          "reasoner/reasoning-model",
		EffortOverride: &high,
	})
	if err != nil {
		t.Fatalf("reasoning SessionConfigState: %v", err)
	}
	if state.EffortOverride == nil || *state.EffortOverride != "high" {
		t.Fatalf("reasoning effort override = %v, want high", state.EffortOverride)
	}

	state, err = (&acpFactory{}).SessionConfigState(context.Background(), acp.SessionConfigStateParams{
		Cwd:            project,
		Model:          "plain/plain-model",
		EffortOverride: &high,
	})
	if err != nil {
		t.Fatalf("plain SessionConfigState: %v", err)
	}
	if _, ok := findACPConfigOption(state.ConfigOptions, "effort"); ok {
		t.Fatalf("plain model should not advertise effort option: %+v", state.ConfigOptions)
	}
	if state.EffortOverride == nil || *state.EffortOverride != "" {
		t.Fatalf("plain effort override = %v, want explicit empty override", state.EffortOverride)
	}
}

func TestACPFactoryPinsRuntimeProfileAndOmitsWorkMode(t *testing.T) {
	isolateCLIConfigHome(t)
	if _, err := config.SetCredential("REASONIX_TEST_KEY", "test-key"); err != nil {
		t.Fatalf("SetCredential: %v", err)
	}
	project := t.TempDir()
	if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
default_model = "local"

[[providers]]
name = "local"
kind = "acp-test-provider"
base_url = "http://example.invalid"
model = "fake-model"
api_key_env = "REASONIX_TEST_KEY"
`), 0o644); err != nil {
		t.Fatal(err)
	}

	state, err := (&acpFactory{}).SessionConfigState(context.Background(), acp.SessionConfigStateParams{
		Cwd:            project,
		RuntimeProfile: "delivery",
	})
	if err != nil {
		t.Fatalf("SessionConfigState: %v", err)
	}
	if _, ok := findACPConfigOption(state.ConfigOptions, "work_mode"); ok {
		t.Fatalf("work_mode should not be advertised: %+v", state.ConfigOptions)
	}
	if state.RuntimeProfile != "balanced" {
		t.Fatalf("RuntimeProfile = %q, want pinned balanced", state.RuntimeProfile)
	}

	state, err = (&acpFactory{}).SessionConfigState(context.Background(), acp.SessionConfigStateParams{Cwd: project})
	if err != nil {
		t.Fatalf("default SessionConfigState: %v", err)
	}
	if state.RuntimeProfile != "balanced" {
		t.Fatalf("default RuntimeProfile = %q, want balanced", state.RuntimeProfile)
	}
}

func TestACPTaskProfileDefaults(t *testing.T) {
	cfg := config.Default()
	cfg.Agent.SubagentModel = "default-model"
	cfg.Agent.SubagentEffort = "high"
	cfg.Agent.SubagentModels = map[string]string{"task": "task-model"}
	cfg.Agent.SubagentEfforts = map[string]string{"task": "max"}

	model, effort := acpTaskProfileDefaults(cfg)
	if model != "task-model" || effort != "max" {
		t.Fatalf("task profile defaults = %q/%q, want task-model/max", model, effort)
	}

	cfg.Agent.SubagentModels = nil
	cfg.Agent.SubagentEfforts = nil
	model, effort = acpTaskProfileDefaults(cfg)
	if model != "default-model" || effort != "high" {
		t.Fatalf("fallback task profile defaults = %q/%q, want default-model/high", model, effort)
	}
}

func TestACPSubagentProviderResolverHonorsProfile(t *testing.T) {
	cfg := config.Default()
	cfg.Providers = []config.ProviderEntry{
		{
			Name:             "parent",
			Kind:             acpTestProviderKind,
			Model:            "parent-model",
			ContextWindow:    111,
			SupportedEfforts: []string{"low", "high"},
		},
		{
			Name:             "sub",
			Kind:             acpTestProviderKind,
			Models:           []string{"sub-model"},
			Default:          "sub-model",
			ContextWindow:    222,
			SupportedEfforts: []string{"low", "high"},
		},
	}
	parent, ok := cfg.ResolveModel("parent")
	if !ok {
		t.Fatal("parent model did not resolve")
	}

	resolve := newACPSubagentProviderResolver(cfg, parent, netclient.ProxySpec{})
	prov, _, ctxWin, err := resolve("sub/sub-model", "HIGH")
	if err != nil {
		t.Fatalf("resolve sub profile: %v", err)
	}
	got := prov.(*acpTestProvider).cfg
	if got.Model != "sub-model" || got.Extra["effort"] != "high" || ctxWin != 222 {
		t.Fatalf("resolved profile = model:%q effort:%v ctx:%d, want sub-model/high/222", got.Model, got.Extra["effort"], ctxWin)
	}

	prov, _, ctxWin, err = resolve("", "low")
	if err != nil {
		t.Fatalf("resolve effort-only profile: %v", err)
	}
	got = prov.(*acpTestProvider).cfg
	if got.Model != "parent-model" || got.Extra["effort"] != "low" || ctxWin != 111 {
		t.Fatalf("effort-only profile = model:%q effort:%v ctx:%d, want parent-model/low/111", got.Model, got.Extra["effort"], ctxWin)
	}
}

func TestACPSubagentProviderResolverRejectsInvalidEffort(t *testing.T) {
	cfg := config.Default()
	cfg.Providers = []config.ProviderEntry{{
		Name:             "parent",
		Kind:             acpTestProviderKind,
		Model:            "parent-model",
		SupportedEfforts: []string{"low", "high"},
	}}
	parent, ok := cfg.ResolveModel("parent")
	if !ok {
		t.Fatal("parent model did not resolve")
	}

	resolve := newACPSubagentProviderResolver(cfg, parent, netclient.ProxySpec{})
	if _, _, _, err := resolve("", "max"); err == nil {
		t.Fatal("invalid effort should fail before ACP task falls back to the parent profile")
	}
}

func findACPConfigOption(options []acp.SessionConfigOption, id string) (acp.SessionConfigOption, bool) {
	for _, opt := range options {
		if opt.ID == id {
			return opt, true
		}
	}
	return acp.SessionConfigOption{}, false
}

func toolMap(tools []tool.Tool) map[string]tool.Tool {
	out := make(map[string]tool.Tool, len(tools))
	for _, t := range tools {
		out[t.Name()] = t
	}
	return out
}

func toolNames(tools map[string]tool.Tool) []string {
	out := make([]string, 0, len(tools))
	for name := range tools {
		out = append(out, name)
	}
	return out
}

type acpTestProvider struct {
	cfg provider.Config
}

func (p *acpTestProvider) Name() string { return p.cfg.Name }

func (p *acpTestProvider) Stream(context.Context, provider.Request) (<-chan provider.Chunk, error) {
	ch := make(chan provider.Chunk, 1)
	ch <- provider.Chunk{Type: provider.ChunkDone}
	close(ch)
	return ch, nil
}
