package plugin

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"

	"reasonix/internal/tool"
)

func TestCanonicalizeSchemaStable(t *testing.T) {
	schema := json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}`)
	first := canonicalizeSchema(schema)
	second := canonicalizeSchema(first)
	if string(first) != string(second) {
		t.Errorf("canonicalizeSchema is not idempotent:\n  first: %s\n  second: %s", first, second)
	}
}

func TestCanonicalizeSchemaSortsRequired(t *testing.T) {
	schema := json.RawMessage(`{"required":["c","a","b"],"type":"object"}`)
	result := canonicalizeSchema(schema)
	var m map[string]any
	json.Unmarshal(result, &m)
	arr := m["required"].([]any)
	if arr[0] != "a" || arr[1] != "b" || arr[2] != "c" {
		t.Errorf("required not sorted: %v", arr)
	}
}

func TestCanonicalizeSchemaPreservesEnum(t *testing.T) {
	schema := json.RawMessage(`{"enum":["c","a","b"]}`)
	result := canonicalizeSchema(schema)
	var m map[string]any
	json.Unmarshal(result, &m)
	arr := m["enum"].([]any)
	if arr[0] != "c" || arr[1] != "a" || arr[2] != "b" {
		t.Errorf("enum order was changed: %v", arr)
	}
}

func TestCanonicalizeSchemaSortsKeys(t *testing.T) {
	schema := json.RawMessage(`{"z":1,"a":2,"m":3}`)
	result := canonicalizeSchema(schema)
	// json.Marshal sorts map keys, so verify the JSON string directly.
	s := string(result)
	if s != `{"a":2,"m":3,"z":1}` {
		t.Errorf("keys not sorted, got: %s", s)
	}
}

func TestCanonicalizeSchemaNested(t *testing.T) {
	schema := json.RawMessage(`{"properties":{"inner":{"type":"object","required":["b","a"]}}}`)
	result := canonicalizeSchema(schema)
	var m map[string]any
	json.Unmarshal(result, &m)
	props := m["properties"].(map[string]any)
	inner := props["inner"].(map[string]any)
	req := inner["required"].([]any)
	if req[0] != "a" || req[1] != "b" {
		t.Errorf("nested required not sorted: %v", req)
	}
}

func TestCanonicalizeSchemaEquivalentOrderingMatches(t *testing.T) {
	first := canonicalizeSchema(json.RawMessage(`{"type":"object","required":["b","a"],"properties":{"b":{"description":"bee","type":"string"},"a":{"type":"integer"}}}`))
	second := canonicalizeSchema(json.RawMessage(`{"properties":{"a":{"type":"integer"},"b":{"type":"string","description":"bee"}},"required":["a","b"],"type":"object"}`))
	if string(first) != string(second) {
		t.Fatalf("equivalent schemas canonicalized differently:\n  first:  %s\n  second: %s", first, second)
	}
}

func TestRemoteToolSchemaCanonicalizesOnReturn(t *testing.T) {
	rt := &remoteTool{schema: json.RawMessage(`{"type":"object","required":["z","a"],"properties":{"z":{"type":"string"},"a":{"type":"string"}}}`)}
	if got, want := string(rt.Schema()), `{"properties":{"a":{"type":"string"},"z":{"type":"string"}},"required":["a","z"],"type":"object"}`; got != want {
		t.Fatalf("Schema() = %s, want %s", got, want)
	}
}

func TestSortToolsByName(t *testing.T) {
	tools := []tool.Tool{
		testTool{name: "zulu"},
		testTool{name: "alpha"},
		testTool{name: "mike"},
	}
	sorted := sortToolsByName(tools)
	if sorted[0].Name() != "alpha" || sorted[1].Name() != "mike" || sorted[2].Name() != "zulu" {
		t.Errorf("tools not sorted: %v", toolNames(sorted))
	}
	// Original should be unchanged
	if tools[0].Name() != "zulu" {
		t.Error("original slice was mutated")
	}
}

func TestNormalizeNameForToolNames(t *testing.T) {
	if got := normalizeName("valid_name-1"); got != "valid_name-1" {
		t.Fatalf("valid name changed: %q", got)
	}
	cases := []string{"@modelcontextprotocol/server-memory", "mcp server/fetch", "   "}
	for _, in := range cases {
		got := normalizeName(in)
		if got == "" || strings.ContainsAny(got, " @/") {
			t.Errorf("normalizeName(%q) = %q, want non-empty safe identifier", in, got)
		}
	}
}

func TestNormalizeNameAvoidsSanitizedCollisions(t *testing.T) {
	a := normalizeName("search/code")
	b := normalizeName("search_code")
	if a == b {
		t.Fatalf("normalized names collided: %q", a)
	}
	if b != "search_code" {
		t.Fatalf("valid identifier should stay stable, got %q", b)
	}
	if normalizeName("@foo") == normalizeName("foo") {
		t.Fatal("trimmed invalid prefix should not collapse onto valid name")
	}
}

func TestSummarizeFailureErrorSingleLine(t *testing.T) {
	got := summarizeFailureError(errors.New("npm error code ENOTEMPTY\nnpm error path /tmp/x"))
	if strings.Contains(got, "\n") || !strings.Contains(got, "ENOTEMPTY") {
		t.Fatalf("summary = %q", got)
	}
}

type testTool struct{ name string }

func (t testTool) Name() string                                                      { return t.name }
func (t testTool) Description() string                                               { return "" }
func (t testTool) Schema() json.RawMessage                                           { return nil }
func (t testTool) Execute(ctx context.Context, args json.RawMessage) (string, error) { return "", nil }
func (t testTool) ReadOnly() bool                                                    { return true }

func toolNames(ts []tool.Tool) []string {
	names := make([]string, len(ts))
	for i, t := range ts {
		names[i] = t.Name()
	}
	return names
}

func getJSONKeys(t *testing.T, raw json.RawMessage) []string {
	t.Helper()
	var m map[string]any
	if err := json.Unmarshal(raw, &m); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}
