package copilot

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"github.com/aoagents/agent-orchestrator/backend/internal/domain"
	"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

func TestManifestID(t *testing.T) {
	got := New().Manifest()
	if got.ID != "copilot" {
		t.Fatalf("Manifest().ID = %q, want %q", got.ID, "copilot")
	}
	if got.Name != "GitHub Copilot" {
		t.Fatalf("Manifest().Name = %q, want %q", got.Name, "GitHub Copilot")
	}
}

func TestGetLaunchCommandBuildsArgv(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	cmd, err := plugin.GetLaunchCommand(context.Background(), ports.LaunchConfig{
		Permissions: ports.PermissionModeBypassPermissions,
		Prompt:      "-fix this",
	})
	if err != nil {
		t.Fatal(err)
	}

	want := []string{"copilot", "--allow-all", "-p", "-fix this"}
	if !reflect.DeepEqual(cmd, want) {
		t.Fatalf("unexpected command\nwant: %#v\n got: %#v", want, cmd)
	}
}

func TestGetLaunchCommandOmitsPromptWhenEmpty(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	cmd, err := plugin.GetLaunchCommand(context.Background(), ports.LaunchConfig{})
	if err != nil {
		t.Fatal(err)
	}
	if contains(cmd, "-p") {
		t.Fatalf("command %#v unexpectedly contains -p", cmd)
	}
}

func TestGetLaunchCommandMapsApprovalModes(t *testing.T) {
	tests := []struct {
		name        string
		permission  ports.PermissionMode
		want        []string
		notExpected []string
	}{
		{
			name:        "default",
			permission:  ports.PermissionModeDefault,
			notExpected: []string{"--allow-tool", "--allow-all-tools", "--allow-all"},
		},
		{
			name:       "accept-edits",
			permission: ports.PermissionModeAcceptEdits,
			want:       []string{"--allow-tool", "write"},
		},
		{
			name:       "auto",
			permission: ports.PermissionModeAuto,
			want:       []string{"--allow-all-tools"},
		},
		{
			name:       "bypass-permissions",
			permission: ports.PermissionModeBypassPermissions,
			want:       []string{"--allow-all"},
		},
		{
			name:        "empty falls back to default",
			permission:  "",
			notExpected: []string{"--allow-tool", "--allow-all-tools", "--allow-all"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := &Plugin{resolvedBinary: "copilot"}
			cmd, err := plugin.GetLaunchCommand(context.Background(), ports.LaunchConfig{
				Permissions: tt.permission,
			})
			if err != nil {
				t.Fatal(err)
			}
			if len(tt.want) > 0 && !containsSubsequence(cmd, tt.want) {
				t.Fatalf("command %#v does not contain %#v", cmd, tt.want)
			}
			for _, ne := range tt.notExpected {
				if contains(cmd, ne) {
					t.Fatalf("command %#v unexpectedly contains %q", cmd, ne)
				}
			}
		})
	}
}

func TestGetLaunchCommandRespectsCanceledContext(t *testing.T) {
	plugin := New()
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	if _, err := plugin.GetLaunchCommand(ctx, ports.LaunchConfig{Prompt: "hi"}); err == nil {
		t.Fatal("GetLaunchCommand with canceled context: err = nil, want non-nil")
	}
}

func TestGetPromptDeliveryStrategyIsInCommand(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	got, err := plugin.GetPromptDeliveryStrategy(context.Background(), ports.LaunchConfig{})
	if err != nil {
		t.Fatal(err)
	}
	if got != ports.PromptDeliveryInCommand {
		t.Fatalf("unexpected strategy: %q", got)
	}
}

func TestGetConfigSpecHasNoCustomFieldsYet(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	spec, err := plugin.GetConfigSpec(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(spec.Fields) != 0 {
		t.Fatalf("unexpected config fields: %#v", spec.Fields)
	}
}

func TestGetRestoreCommandReadsAgentSessionID(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	cmd, ok, err := plugin.GetRestoreCommand(context.Background(), ports.RestoreConfig{
		Permissions: ports.PermissionModeAuto,
		Session: ports.SessionRef{
			Metadata: map[string]string{ports.MetadataKeyAgentSessionID: "uuid-123"},
		},
	})
	if err != nil {
		t.Fatalf("err = %v, want nil", err)
	}
	if !ok {
		t.Fatal("ok = false, want true")
	}
	want := []string{"copilot", "--allow-all-tools", "--resume", "uuid-123"}
	if !reflect.DeepEqual(cmd, want) {
		t.Fatalf("restore cmd\nwant: %#v\n got: %#v", want, cmd)
	}
}

func TestGetRestoreCommandFalseWithoutAgentSessionID(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	cases := []struct {
		name string
		ref  ports.SessionRef
	}{
		{"empty session ref", ports.SessionRef{}},
		{"empty metadata", ports.SessionRef{Metadata: map[string]string{}}},
		{"blank agent session metadata", ports.SessionRef{Metadata: map[string]string{ports.MetadataKeyAgentSessionID: "   "}}},
		{"workspace path only", ports.SessionRef{WorkspacePath: "/some/path"}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			cmd, ok, err := plugin.GetRestoreCommand(context.Background(), ports.RestoreConfig{
				Permissions: ports.PermissionModeAuto,
				Session:     tc.ref,
			})
			if err != nil {
				t.Fatalf("err = %v, want nil", err)
			}
			if ok {
				t.Fatalf("ok = true, want false")
			}
			if cmd != nil {
				t.Fatalf("cmd = %#v, want nil", cmd)
			}
		})
	}
}

func TestSessionInfoReadsHookMetadata(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	info, ok, err := plugin.SessionInfo(context.Background(), ports.SessionRef{
		WorkspacePath: "/some/path",
		Metadata: map[string]string{
			ports.MetadataKeyAgentSessionID: "uuid-123",
			copilotTitleMetadataKey:         "Fix login redirect",
			copilotSummaryMetadataKey:       "Updated the auth callback and tests.",
			"ignored":                       "not returned",
		},
	})
	if err != nil {
		t.Fatalf("err = %v, want nil", err)
	}
	if !ok {
		t.Fatalf("ok = false, want true")
	}
	if info.AgentSessionID != "uuid-123" {
		t.Fatalf("AgentSessionID = %q, want native id", info.AgentSessionID)
	}
	if info.Title != "Fix login redirect" {
		t.Fatalf("Title = %q, want hook title", info.Title)
	}
	if info.Summary != "Updated the auth callback and tests." {
		t.Fatalf("Summary = %q, want hook summary", info.Summary)
	}
	if info.Metadata != nil {
		t.Fatalf("Metadata = %#v, want nil for Copilot", info.Metadata)
	}
}

func TestSessionInfoFalseWhenNoHookMetadata(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}

	info, ok, err := plugin.SessionInfo(context.Background(), ports.SessionRef{
		WorkspacePath: "/some/path",
		Metadata:      map[string]string{},
	})
	if err != nil {
		t.Fatalf("err = %v, want nil", err)
	}
	if ok {
		t.Fatalf("ok = true, want false")
	}
	if !reflect.DeepEqual(info, ports.SessionInfo{}) {
		t.Fatalf("info = %#v, want zero value", info)
	}
}

func TestGetAgentHooksInstallsCopilotHooks(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}
	workspace := t.TempDir()

	hooksPath := copilotHooksPath(workspace)
	if err := os.MkdirAll(filepath.Dir(hooksPath), 0o755); err != nil {
		t.Fatal(err)
	}
	// Seed a user-owned agentStop hook plus an unrelated top-level field; both
	// must survive install.
	existing := `{"version":1,"disableAllHooks":false,"hooks":{"agentStop":[{"type":"command","bash":"custom stop hook","powershell":"custom stop hook"}]}}`
	if err := os.WriteFile(hooksPath, []byte(existing), 0o644); err != nil {
		t.Fatal(err)
	}

	cfg := ports.WorkspaceHookConfig{
		DataDir:       t.TempDir(),
		SessionID:     "sess-1",
		WorkspacePath: workspace,
	}
	if err := plugin.GetAgentHooks(context.Background(), cfg); err != nil {
		t.Fatal(err)
	}
	// A second install must not duplicate AO hook commands.
	if err := plugin.GetAgentHooks(context.Background(), cfg); err != nil {
		t.Fatal(err)
	}

	data, err := os.ReadFile(hooksPath)
	if err != nil {
		t.Fatal(err)
	}
	var file copilotHookFile
	if err := json.Unmarshal(data, &file); err != nil {
		t.Fatal(err)
	}
	if file.Version != copilotHooksVersion {
		t.Fatalf("version = %d, want %d", file.Version, copilotHooksVersion)
	}
	if file.DisableAllHooks == nil || *file.DisableAllHooks {
		t.Fatalf("disableAllHooks not preserved: %#v", file.DisableAllHooks)
	}
	for _, spec := range copilotManagedHooks {
		command := copilotHookCommandPrefix + spec.Command
		if count := countCopilotHookCommand(file.Hooks[spec.Event], command); count != 1 {
			t.Fatalf("%s command count = %d, want 1 in %#v", spec.Event, count, file.Hooks[spec.Event])
		}
	}
	if countCopilotHookCommand(file.Hooks["agentStop"], "custom stop hook") != 1 {
		t.Fatalf("existing agentStop hook was not preserved: %#v", file.Hooks["agentStop"])
	}
}

func TestUninstallHooksRemovesCopilotHooks(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}
	workspace := t.TempDir()
	hooksPath := copilotHooksPath(workspace)

	ctx := context.Background()
	cfg := ports.WorkspaceHookConfig{DataDir: t.TempDir(), SessionID: "sess-1", WorkspacePath: workspace}

	// Pre-seed a user's own agentStop hook; it must survive uninstall.
	if err := os.MkdirAll(filepath.Dir(hooksPath), 0o755); err != nil {
		t.Fatal(err)
	}
	existing := `{"version":1,"hooks":{"agentStop":[{"type":"command","bash":"custom stop hook","powershell":"custom stop hook"}]}}`
	if err := os.WriteFile(hooksPath, []byte(existing), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := plugin.GetAgentHooks(ctx, cfg); err != nil {
		t.Fatal(err)
	}
	if installed, err := plugin.AreHooksInstalled(ctx, workspace); err != nil || !installed {
		t.Fatalf("AreHooksInstalled after install = (%v, %v), want (true, nil)", installed, err)
	}

	if err := plugin.UninstallHooks(ctx, workspace); err != nil {
		t.Fatal(err)
	}
	if installed, err := plugin.AreHooksInstalled(ctx, workspace); err != nil || installed {
		t.Fatalf("AreHooksInstalled after uninstall = (%v, %v), want (false, nil)", installed, err)
	}

	data, err := os.ReadFile(hooksPath)
	if err != nil {
		t.Fatal(err)
	}
	var file copilotHookFile
	if err := json.Unmarshal(data, &file); err != nil {
		t.Fatal(err)
	}
	for _, spec := range copilotManagedHooks {
		command := copilotHookCommandPrefix + spec.Command
		if got := countCopilotHookCommand(file.Hooks[spec.Event], command); got != 0 {
			t.Fatalf("%s command %q count = %d after uninstall, want 0", spec.Event, command, got)
		}
	}
	if countCopilotHookCommand(file.Hooks["agentStop"], "custom stop hook") != 1 {
		t.Fatalf("user agentStop hook not preserved: %#v", file.Hooks["agentStop"])
	}
}

func TestAreHooksInstalledMissingFile(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}
	installed, err := plugin.AreHooksInstalled(context.Background(), t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	if installed {
		t.Fatal("AreHooksInstalled on empty workspace = true, want false")
	}
}

func TestHookMethodsRequireWorkspacePath(t *testing.T) {
	plugin := &Plugin{resolvedBinary: "copilot"}
	ctx := context.Background()

	if err := plugin.GetAgentHooks(ctx, ports.WorkspaceHookConfig{}); err == nil {
		t.Fatal("GetAgentHooks with empty WorkspacePath: err = nil, want non-nil")
	}
	if err := plugin.UninstallHooks(ctx, ""); err == nil {
		t.Fatal("UninstallHooks with empty path: err = nil, want non-nil")
	}
	if _, err := plugin.AreHooksInstalled(ctx, ""); err == nil {
		t.Fatal("AreHooksInstalled with empty path: err = nil, want non-nil")
	}
}

// TestCopilotManagedHooksUseDocumentedEventNames pins the JSON keys AO writes
// into .github/hooks/ao.json to the camelCase names Copilot CLI documents
// (https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks).
// Drifting back to lowercase-dashed or any other casing silently disables the
// hooks, so this is a tripwire for that class of regression.
func TestCopilotManagedHooksUseDocumentedEventNames(t *testing.T) {
	wantEventByCommand := map[string]string{
		"session-start":      "sessionStart",
		"user-prompt-submit": "userPromptSubmitted",
		"permission-request": "preToolUse",
		"stop":               "agentStop",
	}
	if len(copilotManagedHooks) != len(wantEventByCommand) {
		t.Fatalf("copilotManagedHooks length = %d, want %d", len(copilotManagedHooks), len(wantEventByCommand))
	}
	for _, spec := range copilotManagedHooks {
		want, ok := wantEventByCommand[spec.Command]
		if !ok {
			t.Fatalf("unexpected AO sub-command %q in copilotManagedHooks", spec.Command)
		}
		if spec.Event != want {
			t.Fatalf("command %q event = %q, want %q (Copilot CLI documented camelCase)", spec.Command, spec.Event, want)
		}
	}
}

func TestDeriveActivityState(t *testing.T) {
	tests := []struct {
		event     string
		wantState domain.ActivityState
		wantOK    bool
	}{
		{"session-start", domain.ActivityActive, true},
		{"user-prompt-submit", domain.ActivityActive, true},
		{"stop", domain.ActivityIdle, true},
		{"permission-request", domain.ActivityWaitingInput, true},
		{"unknown", "", false},
		{"", "", false},
	}
	for _, tt := range tests {
		t.Run(tt.event, func(t *testing.T) {
			state, ok := DeriveActivityState(tt.event, nil)
			if state != tt.wantState || ok != tt.wantOK {
				t.Fatalf("DeriveActivityState(%q) = (%q, %v), want (%q, %v)", tt.event, state, ok, tt.wantState, tt.wantOK)
			}
		})
	}
}

func contains(values []string, needle string) bool {
	for _, value := range values {
		if value == needle {
			return true
		}
	}
	return false
}

func containsSubsequence(values []string, needle []string) bool {
	if len(needle) == 0 {
		return true
	}
	for start := range values {
		if start+len(needle) > len(values) {
			return false
		}
		ok := true
		for offset, want := range needle {
			if values[start+offset] != want {
				ok = false
				break
			}
		}
		if ok {
			return true
		}
	}
	return false
}

func countCopilotHookCommand(entries []copilotHookEntry, command string) int {
	count := 0
	for _, entry := range entries {
		if entry.Bash == command || entry.Powershell == command {
			count++
		}
	}
	return count
}
