package tools

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"testing"
)

// recordedRequest/newMockClient/respondData helpers live in call_test.go.

func TestListPortsModeSwitch(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"ok": true})
	})
	ctx := context.Background()

	// List mode: no id → primary list endpoint with filters as query.
	if _, err := Dispatch(ctx, c, "list_ports", map[string]any{
		"port":     float64(3306),
		"protocol": "tcp",
		"page":     float64(2),
	}); err != nil {
		t.Fatalf("Dispatch list_ports: %v", err)
	}
	got := (*reqs)[0]
	if got.path != "/api/v1/caasm/ports" {
		t.Errorf("list mode path = %q, want /api/v1/caasm/ports", got.path)
	}
	if got.query.Get("port") != "3306" || got.query.Get("protocol") != "tcp" || got.query.Get("page") != "2" {
		t.Errorf("list mode query = %v, want port=3306&protocol=tcp&page=2", got.query)
	}

	// Detail mode: id switches to ports/{id}, no query params.
	if _, err := Dispatch(ctx, c, "list_ports", map[string]any{"id": float64(7)}); err != nil {
		t.Fatalf("Dispatch list_ports id: %v", err)
	}
	got = (*reqs)[1]
	if got.path != "/api/v1/caasm/ports/7" {
		t.Errorf("detail mode path = %q, want /api/v1/caasm/ports/7", got.path)
	}
	if len(got.query) != 0 {
		t.Errorf("detail mode query = %v, want empty", got.query)
	}
}

func TestListDNSRecordsCommentsMode(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, []any{})
	})
	ctx := context.Background()

	if _, err := Dispatch(ctx, c, "list_dns_records", map[string]any{"has_alert": true}); err != nil {
		t.Fatalf("Dispatch list_dns_records: %v", err)
	}
	if got := (*reqs)[0]; got.path != "/api/v1/caasm/dns-records" || got.query.Get("has_alert") != "true" {
		t.Errorf("list mode = %s?%v, want /api/v1/caasm/dns-records?has_alert=true", got.path, got.query)
	}

	if _, err := Dispatch(ctx, c, "list_dns_records", map[string]any{"id": float64(3)}); err != nil {
		t.Fatalf("Dispatch list_dns_records id: %v", err)
	}
	if got := (*reqs)[1]; got.path != "/api/v1/caasm/dns-records/3/comments" {
		t.Errorf("comments mode path = %q, want /api/v1/caasm/dns-records/3/comments", got.path)
	}
}

func TestGetAssetAggregation(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/caasm/assets/9":
			respondData(w, map[string]any{"id": 9, "type": "host"})
		case "/api/v1/caasm/assets/9/tags":
			respondData(w, []any{"prod", "web"})
		default:
			respondError(w, 404, "not found")
		}
	})

	res, err := Dispatch(context.Background(), c, "get_asset", map[string]any{"id": float64(9)})
	if err != nil {
		t.Fatalf("Dispatch get_asset: %v", err)
	}
	merged, ok := res.(map[string]any)
	if !ok {
		t.Fatalf("result = %#v, want merged object", res)
	}
	asset, ok := merged["asset"].(map[string]any)
	if !ok || asset["type"] != "host" {
		t.Errorf("merged.asset = %#v, want the detail payload", merged["asset"])
	}
	tags, ok := merged["tags"].([]any)
	if !ok || len(tags) != 2 || tags[0] != "prod" {
		t.Errorf("merged.tags = %#v, want [prod web]", merged["tags"])
	}
	if len(*reqs) != 2 {
		t.Errorf("get_asset issued %d requests, want 2 (detail + tags)", len(*reqs))
	}
}

func TestListCloudAccountsDetailAggregation(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/caasm/cloud-accounts/5":
			respondData(w, map[string]any{"id": 5, "provider": "aws"})
		case "/api/v1/caasm/cloud-accounts/5/jobs":
			respondData(w, map[string]any{"list": []any{map[string]any{"id": 100}}, "total": 1})
		default:
			respondData(w, map[string]any{"list": []any{}})
		}
	})
	ctx := context.Background()

	// List mode.
	if _, err := Dispatch(ctx, c, "list_cloud_accounts", map[string]any{"provider": "aws"}); err != nil {
		t.Fatalf("Dispatch list_cloud_accounts: %v", err)
	}
	if got := (*reqs)[0]; got.path != "/api/v1/caasm/cloud-accounts" || got.query.Get("provider") != "aws" {
		t.Errorf("list mode = %s?%v, want /api/v1/caasm/cloud-accounts?provider=aws", got.path, got.query)
	}

	// Detail mode: account + jobs merged.
	res, err := Dispatch(ctx, c, "list_cloud_accounts", map[string]any{"id": float64(5), "page_size": float64(10)})
	if err != nil {
		t.Fatalf("Dispatch list_cloud_accounts id: %v", err)
	}
	merged, ok := res.(map[string]any)
	if !ok {
		t.Fatalf("result = %#v, want merged object", res)
	}
	if account, _ := merged["account"].(map[string]any); account["provider"] != "aws" {
		t.Errorf("merged.account = %#v, want the detail payload", merged["account"])
	}
	if _, ok := merged["jobs"]; !ok {
		t.Errorf("merged has no jobs key: %#v", merged)
	}
	if len(*reqs) != 3 {
		t.Fatalf("detail mode issued %d requests total, want 3 (1 list + 2 detail)", len(*reqs))
	}
	if got := (*reqs)[2]; got.path != "/api/v1/caasm/cloud-accounts/5/jobs" || got.query.Get("page_size") != "10" {
		t.Errorf("jobs request = %s?%v, want page_size propagated", got.path, got.query)
	}
}

func TestListSyncJobsModes(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"ok": true})
	})
	ctx := context.Background()

	// List mode.
	if _, err := Dispatch(ctx, c, "list_sync_jobs", map[string]any{"status": "failed"}); err != nil {
		t.Fatalf("Dispatch list_sync_jobs: %v", err)
	}
	if got := (*reqs)[0]; got.path != "/api/v1/caasm/sync-jobs" || got.query.Get("status") != "failed" {
		t.Errorf("list mode = %s?%v", got.path, got.query)
	}

	// Detail mode: id only.
	if _, err := Dispatch(ctx, c, "list_sync_jobs", map[string]any{"id": float64(11)}); err != nil {
		t.Fatalf("Dispatch list_sync_jobs id: %v", err)
	}
	if got := (*reqs)[1]; got.path != "/api/v1/caasm/sync-jobs/11" {
		t.Errorf("detail mode path = %q", got.path)
	}

	// Events mode: id + event paging args → events endpoint, args as query.
	if _, err := Dispatch(ctx, c, "list_sync_jobs", map[string]any{
		"id": float64(11), "limit": float64(50), "after_id": float64(900),
	}); err != nil {
		t.Fatalf("Dispatch list_sync_jobs events: %v", err)
	}
	got := (*reqs)[2]
	if got.path != "/api/v1/caasm/sync-jobs/11/events" {
		t.Errorf("events mode path = %q", got.path)
	}
	if got.query.Get("limit") != "50" || got.query.Get("after_id") != "900" {
		t.Errorf("events mode query = %v, want limit=50&after_id=900", got.query)
	}
}

func TestListSyncJobsEventArgValidation(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"ok": true})
	})
	ctx := context.Background()

	// before_id and after_id are mutually exclusive.
	_, err := Dispatch(ctx, c, "list_sync_jobs", map[string]any{
		"id": float64(11), "before_id": float64(900), "after_id": float64(100),
	})
	assertArgError(t, err, "互斥")

	// Event paging args require id.
	_, err = Dispatch(ctx, c, "list_sync_jobs", map[string]any{"limit": float64(50)})
	assertArgError(t, err, "id")

	if len(*reqs) != 0 {
		t.Errorf("validation failures issued %d backend requests, want 0", len(*reqs))
	}
}

func TestSearchAssetsIPDimensionSwitch(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"list": []any{}})
	})
	ctx := context.Background()

	// Plain filters → assets endpoint.
	if _, err := Dispatch(ctx, c, "search_assets", map[string]any{"owner": "a@b.c"}); err != nil {
		t.Fatalf("Dispatch search_assets: %v", err)
	}
	if got := (*reqs)[0]; got.path != "/api/v1/caasm/assets" {
		t.Errorf("assets dimension path = %q", got.path)
	}

	// IP-only filters → ip-assets endpoint, serialized as query.
	if _, err := Dispatch(ctx, c, "search_assets", map[string]any{
		"region": "cn-hangzhou", "exposure": "public",
	}); err != nil {
		t.Fatalf("Dispatch search_assets ip: %v", err)
	}
	got := (*reqs)[1]
	if got.path != "/api/v1/caasm/ip-assets" {
		t.Errorf("ip dimension path = %q", got.path)
	}
	if got.query.Get("region") != "cn-hangzhou" || got.query.Get("exposure") != "public" {
		t.Errorf("ip dimension query = %v", got.query)
	}
}

// TestSingleEndpointPassthrough covers the no-switch tools: they hit
// their primary endpoint and return the envelope's data verbatim (no
// field trimming).
func TestSingleEndpointPassthrough(t *testing.T) {
	payload := map[string]any{"nodes": []any{"a", "b"}, "edges": []any{map[string]any{"from": "a", "to": "b"}}}
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, payload)
	})
	ctx := context.Background()

	res, err := Dispatch(ctx, c, "get_relationship_graph", nil)
	if err != nil {
		t.Fatalf("Dispatch get_relationship_graph: %v", err)
	}
	if got := (*reqs)[0]; got.path != "/api/v1/caasm/relationships/graph" {
		t.Errorf("path = %q", got.path)
	}
	m, ok := res.(map[string]any)
	if !ok || len(m) != 2 {
		t.Errorf("result = %#v, want data passed through with all fields", res)
	}
	if _, hasEnvelopeKey := m["code"]; hasEnvelopeKey {
		t.Errorf("result still carries envelope keys: %#v", m)
	}

	for name, wantPath := range map[string]string{
		"list_high_risk_ports":  "/api/v1/caasm/ports/high-risk",
		"list_domains":          "/api/v1/caasm/domains",
		"list_api_route_assets": "/api/v1/caasm/api-assets",
	} {
		before := len(*reqs)
		if _, err := Dispatch(ctx, c, name, nil); err != nil {
			t.Fatalf("Dispatch %s: %v", name, err)
		}
		if got := (*reqs)[before]; got.path != wantPath {
			t.Errorf("%s path = %q, want %q", name, got.path, wantPath)
		}
	}
}

// assertArgError wants err to be a non-nil *ArgError mentioning want.
func assertArgError(t *testing.T, err error, want string) {
	t.Helper()
	if err == nil {
		t.Fatalf("want *ArgError mentioning %q, got nil", want)
	}
	var argErr *ArgError
	if !errors.As(err, &argErr) {
		t.Fatalf("error type = %T, want *ArgError", err)
	}
	if !strings.Contains(err.Error(), want) {
		t.Errorf("error = %q, want it to mention %q", err, want)
	}
}

// TestSearchAssetsViewSwitch covers the dispatch-only view argument:
// it routes to the enum-value endpoints with no query parameters,
// dropping any filters, and rejects illegal values without issuing a
// backend request.
func TestSearchAssetsViewSwitch(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, []any{"prod"})
	})
	ctx := context.Background()

	for view, wantPath := range map[string]string{
		"services":     "/api/v1/caasm/assets/services",
		"environments": "/api/v1/caasm/assets/environments",
	} {
		before := len(*reqs)
		// Filters are ignored in view mode (the endpoints take no params).
		if _, err := Dispatch(ctx, c, "search_assets", map[string]any{"view": view, "owner": "a@b.c"}); err != nil {
			t.Fatalf("Dispatch search_assets view=%s: %v", view, err)
		}
		got := (*reqs)[before]
		if got.path != wantPath {
			t.Errorf("view=%s path = %q, want %q", view, got.path, wantPath)
		}
		if len(got.query) != 0 {
			t.Errorf("view=%s query = %v, want empty (filters dropped)", view, got.query)
		}
	}

	_, err := Dispatch(ctx, c, "search_assets", map[string]any{"view": "bogus"})
	assertArgError(t, err, "合法值")
	if len(*reqs) != 2 {
		t.Errorf("invalid view issued %d backend requests total, want the 2 view calls only", len(*reqs))
	}
}

// TestListPortsViewModes covers view=stats (parameterless) and
// view=by_asset (asset-dimension alias carrying the list filters), plus
// the id/view mutual exclusion.
func TestListPortsViewModes(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"ok": true})
	})
	ctx := context.Background()

	if _, err := Dispatch(ctx, c, "list_ports", map[string]any{"view": "stats", "port": float64(3306)}); err != nil {
		t.Fatalf("Dispatch list_ports view=stats: %v", err)
	}
	got := (*reqs)[0]
	if got.path != "/api/v1/caasm/ports/stats" {
		t.Errorf("stats mode path = %q, want /api/v1/caasm/ports/stats", got.path)
	}
	if len(got.query) != 0 {
		t.Errorf("stats mode query = %v, want empty (filters dropped)", got.query)
	}

	if _, err := Dispatch(ctx, c, "list_ports", map[string]any{"view": "by_asset", "port": float64(3306)}); err != nil {
		t.Fatalf("Dispatch list_ports view=by_asset: %v", err)
	}
	got = (*reqs)[1]
	if got.path != "/api/v1/caasm/assets/ports/search" {
		t.Errorf("by_asset mode path = %q, want /api/v1/caasm/assets/ports/search", got.path)
	}
	if got.query.Get("port") != "3306" {
		t.Errorf("by_asset mode query = %v, want port=3306 propagated", got.query)
	}

	// id (detail mode) and view are mutually exclusive.
	_, err := Dispatch(ctx, c, "list_ports", map[string]any{"id": float64(7), "view": "stats"})
	assertArgError(t, err, "互斥")

	_, err = Dispatch(ctx, c, "list_ports", map[string]any{"view": "bogus"})
	assertArgError(t, err, "合法值")

	if len(*reqs) != 2 {
		t.Errorf("validation failures issued %d backend requests, want the 2 view calls only", len(*reqs))
	}
}

// TestListDomainsViewModes covers view=stats and view=registrars, both
// parameterless, plus the default list mode remaining untouched.
func TestListDomainsViewModes(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"ok": true})
	})
	ctx := context.Background()

	for view, wantPath := range map[string]string{
		"stats":      "/api/v1/caasm/domains/stats",
		"registrars": "/api/v1/caasm/domains/registrars",
	} {
		before := len(*reqs)
		if _, err := Dispatch(ctx, c, "list_domains", map[string]any{"view": view, "status": "active"}); err != nil {
			t.Fatalf("Dispatch list_domains view=%s: %v", view, err)
		}
		got := (*reqs)[before]
		if got.path != wantPath {
			t.Errorf("view=%s path = %q, want %q", view, got.path, wantPath)
		}
		if len(got.query) != 0 {
			t.Errorf("view=%s query = %v, want empty (filters dropped)", view, got.query)
		}
	}

	_, err := Dispatch(ctx, c, "list_domains", map[string]any{"view": "bogus"})
	assertArgError(t, err, "合法值")
	if len(*reqs) != 2 {
		t.Errorf("invalid view issued %d backend requests, want the 2 view calls only", len(*reqs))
	}
}

// TestListAPIRouteAssetsViewConfig covers view=config routing to the
// active discovery-config endpoint.
func TestListAPIRouteAssetsViewConfig(t *testing.T) {
	c, reqs := newMockClient(t, func(w http.ResponseWriter, r *http.Request) {
		respondData(w, map[string]any{"ok": true})
	})
	ctx := context.Background()

	if _, err := Dispatch(ctx, c, "list_api_route_assets", map[string]any{"view": "config", "q": "x"}); err != nil {
		t.Fatalf("Dispatch list_api_route_assets view=config: %v", err)
	}
	got := (*reqs)[0]
	if got.path != "/api/v1/caasm/api-assets/config/active" {
		t.Errorf("config mode path = %q, want /api/v1/caasm/api-assets/config/active", got.path)
	}
	if len(got.query) != 0 {
		t.Errorf("config mode query = %v, want empty (filters dropped)", got.query)
	}

	_, err := Dispatch(ctx, c, "list_api_route_assets", map[string]any{"view": "bogus"})
	assertArgError(t, err, "合法值")
	if len(*reqs) != 1 {
		t.Errorf("invalid view issued %d backend requests, want the 1 config call only", len(*reqs))
	}
}
