package tool

import (
	"context"
	"strings"
	"testing"
)

func TestBuildAll_KnownTools(t *testing.T) {
	tools, err := BuildAll([]string{"retrieval", "wikipedia"}, nil)
	if err != nil {
		t.Fatalf("BuildAll: %v", err)
	}
	if len(tools) != 2 {
		t.Fatalf("len(tools) = %d, want 2", len(tools))
	}
	info0, err := tools[0].Info(context.Background())
	if err != nil {
		t.Fatalf("tools[0].Info: %v", err)
	}
	if info0.Name != "search_my_dateset" {
		t.Errorf("tools[0].Info().Name = %q, want search_my_dateset", info0.Name)
	}
	info1, err := tools[1].Info(context.Background())
	if err != nil {
		t.Fatalf("tools[1].Info: %v", err)
	}
	if info1.Name != "wikipedia_search" {
		t.Errorf("tools[1].Info().Name = %q, want wikipedia_search", info1.Name)
	}
}

func TestBuildAll_UnknownTool(t *testing.T) {
	_, err := BuildAll([]string{"does_not_exist"}, nil)
	if err == nil {
		t.Fatal("expected error for unknown tool")
	}
	if !strings.Contains(err.Error(), `unsupported tool "does_not_exist"`) {
		t.Fatalf("err = %q, want unsupported tool message", err.Error())
	}
}

func TestBuildByName_TavilyCanvasComponentNames(t *testing.T) {
	for _, tc := range []struct {
		name string
	}{
		{name: "TavilySearch"},
		{name: "TavilyExtract"},
	} {
		built, err := BuildByName(tc.name, nil)
		if err != nil {
			t.Fatalf("BuildByName(%q): %v", tc.name, err)
		}
		switch tc.name {
		case "TavilySearch":
			if _, ok := built.(*TavilyTool); !ok {
				t.Errorf("BuildByName(%q) returned %T, want *TavilyTool", tc.name, built)
			}
		case "TavilyExtract":
			if _, ok := built.(*TavilyExtractTool); !ok {
				t.Errorf("BuildByName(%q) returned %T, want *TavilyExtractTool", tc.name, built)
			}
		}
	}
}

func TestBuildByName_QueritAliases(t *testing.T) {
	for _, name := range []string{"querit", "querit_search", "queritsearch", "QueritSearch"} {
		built, err := BuildByName(name, map[string]any{
			"api_key":          "stored-key",
			"count":            float64(8),
			"chunks_per_doc":   float64(2),
			"site_include":     []any{"example.com"},
			"site_exclude":     []string{"blocked.example"},
			"time_range":       "d7",
			"country_include":  []any{"CN"},
			"language_include": []any{"zh"},
			"outputs":          map[string]any{"json": map[string]any{}},
		})
		if err != nil {
			t.Fatalf("BuildByName(%q): %v", name, err)
		}
		querit, ok := built.(*QueritTool)
		if !ok {
			t.Fatalf("BuildByName(%q) returned %T, want *QueritTool", name, built)
		}
		if querit.defaults.Count != 8 || querit.defaults.ChunksPerDoc == nil || *querit.defaults.ChunksPerDoc != 2 || len(querit.defaults.SiteInclude) != 1 {
			t.Fatalf("BuildByName(%q) defaults = %#v", name, querit.defaults)
		}
		info, infoErr := built.Info(context.Background())
		if infoErr != nil || info.Name != "querit_search" {
			t.Fatalf("BuildByName(%q).Info() = %#v, %v", name, info, infoErr)
		}
	}
}

func TestBuildAll_AllRegisteredTools(t *testing.T) {
	// Every key in registry.
	names := []string{
		"akshare", "arxiv", "bgpt", "code_exec", "crawler", "deepl",
		"duckduckgo", "email", "exesql", "execute_sql", "github", "google",
		"google_scholar", "google_scholar_search", "jin10", "keenable", "pubmed", "qweather",
		"querit", "querit_search", "queritsearch",
		"retrieval", "search_my_dataset", "search_my_dateset", "searxng",
		"tavily", "tavily_extract", "tushare", "web_crawler", "wencai", "wikipedia", "wikipedia_search",
		"yahoo_finance",
	}
	params := map[string]map[string]any{
		"execute_sql": {
			"db_type":     "mysql",
			"host":        "127.0.0.1",
			"port":        3306,
			"database":    "demo",
			"username":    "u",
			"password":    "p",
			"max_records": 10,
		},
		"exesql": {
			"db_type":     "mysql",
			"host":        "127.0.0.1",
			"port":        3306,
			"database":    "demo",
			"username":    "u",
			"password":    "p",
			"max_records": 10,
		},
		"keenable": {
			"api_key": "key-test",
		},
	}
	tools, err := BuildAll(names, params)
	if err != nil {
		t.Fatalf("BuildAll(all registered): %v", err)
	}
	if len(tools) != len(names) {
		t.Fatalf("len(tools) = %d, want %d", len(tools), len(names))
	}
}

func TestBuildAll_ExeSQLRequiresNodeParams(t *testing.T) {
	_, err := BuildAll([]string{"execute_sql"}, nil)
	if err == nil {
		t.Fatal("expected execute_sql config error")
	}
	if !strings.Contains(err.Error(), "execute_sql requires node-level params") {
		t.Fatalf("err = %q, want execute_sql config error", err.Error())
	}
}

// TestToolRegistry_SchemasAreComplete sweeps every name the public
// registry advertises (including the execute_sql/exesql and
// retrieval/search_my_dateset alias pairs), builds the tool, and
// asserts that its Info() returns a complete schema — non-empty
// Name and Desc, non-nil ParamsOneOf, and a consistent canonical
// name across alias entries. Catches drift like "tool renamed but
// registry not updated", "param added but schema not updated",
// "tool registered with empty description", and "alias points to
// the wrong canonical name".
func TestToolRegistry_SchemasAreComplete(t *testing.T) {
	t.Parallel()

	// Every entry the registry advertises.
	names := []string{
		"akshare", "arxiv", "bgpt", "code_exec", "crawler", "deepl",
		"duckduckgo", "email", "execute_sql", "exesql", "github", "google",
		"google_scholar", "google_scholar_search", "jin10", "keenable", "pubmed", "qweather",
		"querit", "querit_search", "queritsearch",
		"retrieval", "search_my_dataset", "search_my_dateset", "searxng",
		"tavily", "tavily_extract", "tushare", "web_crawler", "wencai", "wikipedia", "wikipedia_search",
		"yahoo_finance",
	}
	params := map[string]map[string]any{
		"execute_sql": {
			"db_type":     "mysql",
			"host":        "127.0.0.1",
			"port":        3306,
			"database":    "demo",
			"username":    "u",
			"password":    "p",
			"max_records": 10,
		},
		"exesql": {
			"db_type":     "mysql",
			"host":        "127.0.0.1",
			"port":        3306,
			"database":    "demo",
			"username":    "u",
			"password":    "p",
			"max_records": 10,
		},
		"keenable": {
			"api_key": "key-xyz",
		},
	}
	tools, err := BuildAll(names, params)
	if err != nil {
		t.Fatalf("BuildAll(%d names): %v", len(names), err)
	}
	if len(tools) != len(names) {
		t.Fatalf("BuildAll returned %d tools for %d names", len(tools), len(names))
	}

	// Schema-level checks per entry.
	for i, name := range names {
		info, err := tools[i].Info(context.Background())
		if err != nil {
			t.Errorf("tools[%d] (registry name %q).Info: %v", i, name, err)
			continue
		}
		if info.Name == "" {
			t.Errorf("tools[%d] (registry name %q).Info().Name is empty", i, name)
		}
		if info.Desc == "" {
			t.Errorf("tools[%d] (registry name %q).Info().Desc is empty", i, name)
		}
		if info.ParamsOneOf == nil {
			t.Errorf("tools[%d] (registry name %q).Info().ParamsOneOf is nil", i, name)
		}
	}

	// Alias consistency: execute_sql and exesql must surface the
	// same canonical Info().Name; same for retrieval/search_my_dataset/
	// search_my_dataset and crawler/web_crawler. A bug here would mean
	// an alias was accidentally pointed at a different tool.
	canonicalByAlias := map[string]string{
		"execute_sql":           "execute_sql",
		"exesql":                "execute_sql",
		"google_scholar":        "google_scholar_search",
		"google_scholar_search": "google_scholar_search",
		"retrieval":             "search_my_dateset",
		"search_my_dataset":     "search_my_dateset",
		"search_my_dateset":     "search_my_dateset",
		"crawler":               "web_crawler",
		"web_crawler":           "web_crawler",
		"wikipedia":             "wikipedia_search",
		"wikipedia_search":      "wikipedia_search",
		"querit":                "querit_search",
		"querit_search":         "querit_search",
		"queritsearch":          "querit_search",
	}
	for _, name := range names {
		canonical, ok := canonicalByAlias[name]
		if !ok {
			continue
		}
		idx := indexOf(names, name)
		info, err := tools[idx].Info(context.Background())
		if err != nil {
			continue
		}
		if info.Name != canonical {
			t.Errorf("registry name %q: Info().Name = %q, want %q (alias must surface canonical name)",
				name, info.Name, canonical)
		}
	}
}

// indexOf returns the index of s in xs, or -1 if not present.
// Tiny helper to keep the alias loop above free of a slice lookup
// closure; the test's names slice is <30 items so linear scan is
// fine.
func indexOf(xs []string, s string) int {
	for i, x := range xs {
		if x == s {
			return i
		}
	}
	return -1
}
