package acp

import (
	"context"
	"encoding/json"
	"path/filepath"
	"strings"
	"testing"
)

func TestFlattenPrompt(t *testing.T) {
	tests := []struct {
		name   string
		blocks []ContentBlock
		want   string
	}{
		{
			name:   "text blocks join with blank line",
			blocks: []ContentBlock{{Type: "text", Text: "hello"}, {Type: "text", Text: "world"}},
			want:   "hello\n\nworld",
		},
		{
			name: "resource contributes inline text",
			blocks: []ContentBlock{
				{Type: "text", Text: "see file:"},
				{Type: "resource", Resource: &ResourceContents{URI: "file:///x", Text: "contents"}},
			},
			want: "see file:\n\ncontents",
		},
		{
			name: "resource without inline text is dropped",
			blocks: []ContentBlock{
				{Type: "resource", Resource: &ResourceContents{URI: "file:///x"}},
				{Type: "text", Text: "only this"},
			},
			want: "only this",
		},
		{
			name: "image and audio blocks are ignored",
			blocks: []ContentBlock{
				{Type: "image", MimeType: "image/png", Data: "base64"},
				{Type: "text", Text: "kept"},
				{Type: "audio", MimeType: "audio/wav", Data: "base64"},
			},
			want: "kept",
		},
		{
			name:   "surrounding whitespace trimmed",
			blocks: []ContentBlock{{Type: "text", Text: "  spaced  "}},
			want:   "spaced",
		},
		{
			name:   "empty input",
			blocks: nil,
			want:   "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := FlattenPrompt(tt.blocks); got != tt.want {
				t.Errorf("FlattenPrompt() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestToolKindFor(t *testing.T) {
	tests := map[string]string{
		"read_file":             "read",
		"ls":                    "read",
		"glob":                  "read",
		"grep":                  "search",
		"edit_file":             "edit",
		"move_file":             "edit",
		"multiedit":             "edit",
		"write_file":            "edit",
		"bash":                  "execute",
		"webfetch":              "other",
		"task":                  "other",
		"mcp__server__do_thing": "other",
		"semantic_search":       "search", // heuristic fallback
		"run_command":           "execute",
		"unknown":               "other",
	}
	for name, want := range tests {
		if got := toolKindFor(name); got != want {
			t.Errorf("toolKindFor(%q) = %q, want %q", name, got, want)
		}
	}
}

// newSessionID

func TestNewSessionID(t *testing.T) {
	id, err := newSessionID()
	if err != nil {
		t.Fatalf("newSessionID: %v", err)
	}
	parts := strings.Split(id, "-")
	if len(parts) != 5 {
		t.Fatalf("UUID format: %q has %d parts, want 5", id, len(parts))
	}
	if len(parts[0]) != 8 || len(parts[1]) != 4 || len(parts[2]) != 4 || len(parts[3]) != 4 || len(parts[4]) != 12 {
		t.Errorf("UUID part lengths: %v", parts)
	}
	// Version 4: bits 4-7 of byte 6 are 0100.
	if id[14] != '4' {
		t.Errorf("UUID version: char at 14 = %c, want '4'", id[14])
	}
	// Variant: bits 6-7 of byte 8 are 10.
	variant := id[19]
	if variant != '8' && variant != '9' && variant != 'a' && variant != 'b' {
		t.Errorf("UUID variant: char at 19 = %c, want 8/9/a/b", variant)
	}
}

func TestNewSessionIDUnique(t *testing.T) {
	seen := map[string]bool{}
	for range 100 {
		id, err := newSessionID()
		if err != nil {
			t.Fatalf("newSessionID: %v", err)
		}
		if seen[id] {
			t.Fatalf("duplicate session id: %s", id)
		}
		seen[id] = true
	}
}

// mcpSpecs

func TestMcpSpecsNil(t *testing.T) {
	if got, err := mcpSpecs(nil, ""); err != nil || got != nil {
		t.Errorf("mcpSpecs(nil) = %v, want nil", got)
	}
	if got, err := mcpSpecs([]MCPServerSpec{}, ""); err != nil || got != nil {
		t.Errorf("mcpSpecs([]) = %v, want nil", got)
	}
}

func TestMcpSpecsConversion(t *testing.T) {
	in := []MCPServerSpec{
		{Name: "search", Command: "search-mcp", Args: []string{"--stdio"}, Env: MCPEnv{"HOME": "/tmp"}},
		{Name: "remote", Type: "http", URL: "https://mcp.example.test", Headers: MCPHeaders{"Authorization": "Bearer token"}},
	}
	got, err := mcpSpecs(in, "/workspace")
	if err != nil {
		t.Fatalf("mcpSpecs: %v", err)
	}
	if len(got) != 2 {
		t.Fatalf("len = %d, want 2", len(got))
	}
	if got[0].Name != "search" || got[0].Type != "stdio" || got[0].Command != "search-mcp" {
		t.Errorf("spec = %+v", got[0])
	}
	if got[0].Args[0] != "--stdio" {
		t.Errorf("args = %v", got[0].Args)
	}
	if got[0].Env["HOME"] != "/tmp" {
		t.Errorf("env = %v", got[0].Env)
	}
	if got[0].Dir != "/workspace" {
		t.Errorf("dir = %q, want /workspace", got[0].Dir)
	}
	if got[0].WorkspaceRoot != "/workspace" || got[1].WorkspaceRoot != "/workspace" {
		t.Errorf("workspace roots = %q, %q, want /workspace", got[0].WorkspaceRoot, got[1].WorkspaceRoot)
	}
	if got[1].Name != "remote" || got[1].Type != "http" || got[1].URL != "https://mcp.example.test" {
		t.Errorf("http spec = %+v", got[1])
	}
	if got[1].Headers["Authorization"] != "Bearer token" {
		t.Errorf("headers = %v", got[1].Headers)
	}
}

func TestMCPEnvAcceptsOfficialArrayShape(t *testing.T) {
	var p SessionNewParams
	raw := []byte(`{
		"cwd":"/tmp",
		"mcpServers":[{
			"name":"fs",
			"command":"mcp-fs",
			"args":["--stdio"],
			"env":[{"name":"HOME","value":"/tmp"},{"name":"EMPTY","value":""}]
		}]
	}`)
	if err := json.Unmarshal(raw, &p); err != nil {
		t.Fatalf("unmarshal official env array: %v", err)
	}
	got, err := mcpSpecs(p.MCPServers, p.Cwd)
	if err != nil {
		t.Fatalf("mcpSpecs: %v", err)
	}
	if got[0].Env["HOME"] != "/tmp" || got[0].Env["EMPTY"] != "" {
		t.Fatalf("env = %v, want HOME and EMPTY from official array", got[0].Env)
	}
}

func TestMCPHeadersAcceptsOfficialArrayShape(t *testing.T) {
	var p SessionNewParams
	raw := []byte(`{
		"cwd":"/tmp",
		"mcpServers":[{
			"name":"remote",
			"type":"http",
			"url":"https://mcp.example.test",
			"headers":[{"name":"Authorization","value":"Bearer token"},{"name":"X-Trace","value":""}]
		}]
	}`)
	if err := json.Unmarshal(raw, &p); err != nil {
		t.Fatalf("unmarshal official headers array: %v", err)
	}
	got, err := mcpSpecs(p.MCPServers, p.Cwd)
	if err != nil {
		t.Fatalf("mcpSpecs: %v", err)
	}
	if got[0].Headers["Authorization"] != "Bearer token" || got[0].Headers["X-Trace"] != "" {
		t.Fatalf("headers = %v, want Authorization and X-Trace from official array", got[0].Headers)
	}
}

func TestMCPHeadersAcceptsEmptyArray(t *testing.T) {
	var p SessionNewParams
	raw := []byte(`{
		"cwd":"/tmp",
		"mcpServers":[{
			"name":"remote",
			"type":"http",
			"url":"https://mcp.example.test",
			"headers":[]
		}]
	}`)
	if err := json.Unmarshal(raw, &p); err != nil {
		t.Fatalf("unmarshal empty headers array (paseo-shape): %v", err)
	}
	if len(p.MCPServers[0].Headers) != 0 {
		t.Fatalf("headers = %v, want empty", p.MCPServers[0].Headers)
	}
}

func TestMCPHeadersAcceptsLegacyMap(t *testing.T) {
	var p SessionNewParams
	raw := []byte(`{
		"cwd":"/tmp",
		"mcpServers":[{
			"name":"remote",
			"type":"http",
			"url":"https://mcp.example.test",
			"headers":{"Authorization":"Bearer token"}
		}]
	}`)
	if err := json.Unmarshal(raw, &p); err != nil {
		t.Fatalf("unmarshal legacy headers map: %v", err)
	}
	if p.MCPServers[0].Headers["Authorization"] != "Bearer token" {
		t.Fatalf("headers = %v, want legacy-map value", p.MCPServers[0].Headers)
	}
}

func TestMcpSpecsRejectsUnsupportedTransport(t *testing.T) {
	got, err := mcpSpecs([]MCPServerSpec{{Name: "remote", Type: "sse", URL: "https://example.test/sse"}}, "/tmp")
	if err != nil || len(got) != 1 || got[0].Type != "sse" {
		t.Fatalf("mcpSpecs legacy SSE = %+v, %v", got, err)
	}
	_, err = mcpSpecs([]MCPServerSpec{{Name: "remote", Type: "websocket", URL: "https://example.test/ws"}}, "/tmp")
	if err == nil || !strings.Contains(err.Error(), "unsupported transport") {
		t.Fatalf("mcpSpecs unsupported transport err = %v", err)
	}
	_, err = mcpSpecs([]MCPServerSpec{{Name: "remote", Type: "http"}}, "/tmp")
	if err == nil || !strings.Contains(err.Error(), "url is required") {
		t.Fatalf("mcpSpecs missing url err = %v", err)
	}
}

// transcriptPath

func TestTranscriptPath(t *testing.T) {
	dir := t.TempDir()
	got := transcriptPath(dir, "abc-123")
	if want := filepath.Join(dir, "abc-123.jsonl"); got != want {
		t.Errorf("transcriptPath = %q, want %q", got, want)
	}
}

// Protocol constants

func TestProtocolVersion(t *testing.T) {
	if ProtocolVersion != 1 {
		t.Errorf("ProtocolVersion = %d", ProtocolVersion)
	}
}

func TestErrorCodes(t *testing.T) {
	if ErrParse != -32700 {
		t.Errorf("ErrParse = %d", ErrParse)
	}
	if ErrInvalidRequest != -32600 {
		t.Errorf("ErrInvalidRequest = %d", ErrInvalidRequest)
	}
	if ErrMethodNotFound != -32601 {
		t.Errorf("ErrMethodNotFound = %d", ErrMethodNotFound)
	}
	if ErrInvalidParams != -32602 {
		t.Errorf("ErrInvalidParams = %d", ErrInvalidParams)
	}
	if ErrInternal != -32603 {
		t.Errorf("ErrInternal = %d", ErrInternal)
	}
}

// acpSession

func TestAcpSessionSetCancelAbort(t *testing.T) {
	sess := &acpSession{id: "test"}
	aborted := false
	_, cancel, ok := sess.begin(context.Background())
	if !ok {
		t.Fatal("begin should succeed")
	}
	sess.mu.Lock()
	sess.cancel = func() {
		aborted = true
		cancel()
	}
	sess.mu.Unlock()
	sess.abort()
	if !aborted {
		t.Error("abort should call the cancel func")
	}
}

func TestAcpSessionAbortNil(t *testing.T) {
	sess := &acpSession{id: "test"}
	sess.abort() // should not panic
}

func TestAcpSessionSetCancelNil(t *testing.T) {
	sess := &acpSession{id: "test"}
	sess.finish()
	sess.abort() // should not panic
}
