package tools

import (
	"context"
	"testing"
)

// ── Predicate Tests ─────────────────────────

func TestAlwaysAvailable(t *testing.T) {
	cases := []ToolContext{
		{},
		{ChannelType: "direct"},
		{IsVisitor: true},
		{WorkspaceID: "ws-1", WorkflowID: "wf-1", TeamID: "t-1"},
	}
	for i, tc := range cases {
		if !AlwaysAvailable(tc) {
			t.Errorf("case %d: AlwaysAvailable returned false", i)
		}
	}
}

func TestRequireWorkspace(t *testing.T) {
	if RequireWorkspace(ToolContext{}) {
		t.Error("Expected false when WorkspaceID is empty")
	}
	if !RequireWorkspace(ToolContext{WorkspaceID: "ws-123"}) {
		t.Error("Expected true when WorkspaceID is set")
	}
}

func TestRequireWorkflow(t *testing.T) {
	if RequireWorkflow(ToolContext{}) {
		t.Error("Expected false when WorkflowID is empty")
	}
	if !RequireWorkflow(ToolContext{WorkflowID: "wf-456"}) {
		t.Error("Expected true when WorkflowID is set")
	}
}

func TestRequireTeam(t *testing.T) {
	if RequireTeam(ToolContext{}) {
		t.Error("Expected false when TeamID is empty")
	}
	if !RequireTeam(ToolContext{TeamID: "team-1"}) {
		t.Error("Expected true when TeamID is set")
	}
}

func TestDenyVisitor(t *testing.T) {
	if !DenyVisitor(ToolContext{IsVisitor: false}) {
		t.Error("Expected true for authenticated user")
	}
	if DenyVisitor(ToolContext{IsVisitor: true}) {
		t.Error("Expected false for visitor")
	}
}

func TestAll(t *testing.T) {
	wsAndNonVisitor := All(RequireWorkspace, DenyVisitor)

	cases := []struct {
		name   string
		tc     ToolContext
		expect bool
	}{
		{"both satisfied", ToolContext{WorkspaceID: "ws-1", IsVisitor: false}, true},
		{"no workspace", ToolContext{WorkspaceID: "", IsVisitor: false}, false},
		{"is visitor", ToolContext{WorkspaceID: "ws-1", IsVisitor: true}, false},
		{"neither satisfied", ToolContext{WorkspaceID: "", IsVisitor: true}, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := wsAndNonVisitor(tc.tc); got != tc.expect {
				t.Errorf("got %v, want %v", got, tc.expect)
			}
		})
	}
}

func TestAllEmpty(t *testing.T) {
	// All() with no predicates should pass (vacuous truth)
	pred := All()
	if !pred(ToolContext{}) {
		t.Error("All() with no predicates should return true")
	}
}

func TestAny(t *testing.T) {
	wsOrWorkflow := Any(RequireWorkspace, RequireWorkflow)

	cases := []struct {
		name   string
		tc     ToolContext
		expect bool
	}{
		{"workspace only", ToolContext{WorkspaceID: "ws-1"}, true},
		{"workflow only", ToolContext{WorkflowID: "wf-1"}, true},
		{"both", ToolContext{WorkspaceID: "ws-1", WorkflowID: "wf-1"}, true},
		{"neither", ToolContext{}, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := wsOrWorkflow(tc.tc); got != tc.expect {
				t.Errorf("got %v, want %v", got, tc.expect)
			}
		})
	}
}

func TestAnyEmpty(t *testing.T) {
	// Any() with no predicates should fail (no match)
	pred := Any()
	if pred(ToolContext{}) {
		t.Error("Any() with no predicates should return false")
	}
}

func TestNot(t *testing.T) {
	notVisitor := Not(func(tc ToolContext) bool { return tc.IsVisitor })
	if !notVisitor(ToolContext{IsVisitor: false}) {
		t.Error("Not(visitor) should return true for non-visitor")
	}
	if notVisitor(ToolContext{IsVisitor: true}) {
		t.Error("Not(visitor) should return false for visitor")
	}
}

func TestComposedPredicates(t *testing.T) {
	// workspace_create: available when NO workspace AND not visitor
	workspaceCreate := All(Not(RequireWorkspace), DenyVisitor)

	cases := []struct {
		name   string
		tc     ToolContext
		expect bool
	}{
		{"no ws, authenticated", ToolContext{}, true},
		{"has ws, authenticated", ToolContext{WorkspaceID: "ws-1"}, false},
		{"no ws, visitor", ToolContext{IsVisitor: true}, false},
		{"has ws, visitor", ToolContext{WorkspaceID: "ws-1", IsVisitor: true}, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := workspaceCreate(tc.tc); got != tc.expect {
				t.Errorf("got %v, want %v", got, tc.expect)
			}
		})
	}
}

// ── BaseTool Tests ──────────────────────────

type testToolWithBase struct {
	BaseTool
}

func (t *testToolWithBase) Definition() ToolDef {
	return ToolDef{
		Name:        "always_tool",
		Description: "test tool with BaseTool embed",
		Parameters:  MustJSON(map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}),
	}
}

func (t *testToolWithBase) Execute(_ context.Context, _ ExecutionContext, _ string) (string, error) {
	return `{"ok":true}`, nil
}

func TestBaseToolSatisfiesContextualTool(t *testing.T) {
	tool := &testToolWithBase{}

	// Must satisfy ContextualTool
	ct, ok := interface{}(tool).(ContextualTool)
	if !ok {
		t.Fatal("testToolWithBase should satisfy ContextualTool interface")
	}

	// BaseTool.Availability() should return AlwaysAvailable
	if !ct.Availability()(ToolContext{IsVisitor: true}) {
		t.Error("BaseTool.Availability() should be AlwaysAvailable")
	}
}

// ── AvailableFor Tests ──────────────────────

// testContextTool overrides availability with a custom predicate.
type testContextTool struct {
	BaseTool
	name string
	req  Require
}

func (t *testContextTool) Definition() ToolDef {
	return ToolDef{
		Name:        t.name,
		Description: "test contextual tool: " + t.name,
		Parameters:  MustJSON(map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}),
	}
}

func (t *testContextTool) Execute(_ context.Context, _ ExecutionContext, _ string) (string, error) {
	return `{"ok":true}`, nil
}

func (t *testContextTool) Availability() Require {
	return t.req
}

// testPlainTool is a tool that does NOT implement ContextualTool —
// only the base Tool interface. It should always be included.
type testPlainTool struct {
	name string
}

func (t *testPlainTool) Definition() ToolDef {
	return ToolDef{
		Name:        t.name,
		Description: "plain tool: " + t.name,
		Parameters:  MustJSON(map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}),
	}
}

func (t *testPlainTool) Execute(_ context.Context, _ ExecutionContext, _ string) (string, error) {
	return `{"ok":true}`, nil
}

func TestAvailableFor(t *testing.T) {
	// Save and restore global registry
	origRegistry := registry
	defer func() { registry = origRegistry }()

	registry = map[string]Tool{
		"always_tool":    &testToolWithBase{},
		"ws_tool":        &testContextTool{name: "ws_tool", req: RequireWorkspace},
		"visitor_denied": &testContextTool{name: "visitor_denied", req: DenyVisitor},
		"ws_no_visitor":  &testContextTool{name: "ws_no_visitor", req: All(RequireWorkspace, DenyVisitor)},
		"plain_tool":     &testPlainTool{name: "plain_tool"},
	}

	cases := []struct {
		name     string
		tctx     ToolContext
		disabled map[string]bool
		expect   []string // expected tool names (sorted doesn't matter, just set membership)
	}{
		{
			name:   "empty context — only always + plain",
			tctx:   ToolContext{},
			expect: []string{"always_tool", "visitor_denied", "plain_tool"},
		},
		{
			name:   "workspace context — ws tools available",
			tctx:   ToolContext{WorkspaceID: "ws-1"},
			expect: []string{"always_tool", "ws_tool", "visitor_denied", "ws_no_visitor", "plain_tool"},
		},
		{
			name:   "visitor with workspace — visitor_denied and ws_no_visitor excluded",
			tctx:   ToolContext{WorkspaceID: "ws-1", IsVisitor: true},
			expect: []string{"always_tool", "ws_tool", "plain_tool"},
		},
		{
			name:     "disabled tool excluded",
			tctx:     ToolContext{WorkspaceID: "ws-1"},
			disabled: map[string]bool{"ws_tool": true},
			expect:   []string{"always_tool", "visitor_denied", "ws_no_visitor", "plain_tool"},
		},
		{
			name:   "visitor no workspace — minimal set",
			tctx:   ToolContext{IsVisitor: true},
			expect: []string{"always_tool", "plain_tool"},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			disabled := tc.disabled
			if disabled == nil {
				disabled = map[string]bool{}
			}
			defs := AvailableFor(tc.tctx, disabled)

			got := make(map[string]bool, len(defs))
			for _, d := range defs {
				got[d.Name] = true
			}

			// Check expected tools are present
			for _, name := range tc.expect {
				if !got[name] {
					t.Errorf("expected tool %q not found in result", name)
				}
			}
			// Check no unexpected tools
			expect := make(map[string]bool, len(tc.expect))
			for _, name := range tc.expect {
				expect[name] = true
			}
			for name := range got {
				if !expect[name] {
					t.Errorf("unexpected tool %q in result", name)
				}
			}
		})
	}
}

func TestAvailableForBackwardCompat(t *testing.T) {
	// AvailableFor with empty ToolContext should behave like
	// the old AllDefinitionsFiltered for plain tools
	origRegistry := registry
	defer func() { registry = origRegistry }()

	registry = map[string]Tool{
		"tool_a": &testPlainTool{name: "tool_a"},
		"tool_b": &testPlainTool{name: "tool_b"},
		"tool_c": &testPlainTool{name: "tool_c"},
	}

	// No disabled, empty context — all plain tools should appear
	defs := AvailableFor(ToolContext{}, map[string]bool{})
	if len(defs) != 3 {
		t.Errorf("Expected 3 tools, got %d", len(defs))
	}

	// Disable one
	defs = AvailableFor(ToolContext{}, map[string]bool{"tool_b": true})
	if len(defs) != 2 {
		t.Errorf("Expected 2 tools, got %d", len(defs))
	}
	for _, d := range defs {
		if d.Name == "tool_b" {
			t.Error("tool_b should be filtered out")
		}
	}
}

// ── ToolContext from ExecutionContext ────────

func TestToolContextFromExecContext(t *testing.T) {
	// Verify the fields map correctly between the two structs.
	// This is a compile-time/structural test — when ToolContext is
	// built from ExecutionContext in the completion handler, these
	// fields must align.
	exec := ExecutionContext{
		UserID:      "u-1",
		ChannelID:   "ch-1",
		PersonaID:   "p-1",
		WorkspaceID: "ws-1",
		WorkflowID:  "wf-1",
		TeamID:      "t-1",
	}

	// Simulate what the completion handler will do
	tc := ToolContext{
		WorkspaceID: exec.WorkspaceID,
		WorkflowID:  exec.WorkflowID,
		TeamID:      exec.TeamID,
		PersonaID:   exec.PersonaID,
		// ChannelType and IsVisitor come from channel record / gin context
	}

	if tc.WorkspaceID != "ws-1" {
		t.Errorf("WorkspaceID mismatch: %q", tc.WorkspaceID)
	}
	if tc.WorkflowID != "wf-1" {
		t.Errorf("WorkflowID mismatch: %q", tc.WorkflowID)
	}
	if tc.TeamID != "t-1" {
		t.Errorf("TeamID mismatch: %q", tc.TeamID)
	}
	if tc.PersonaID != "p-1" {
		t.Errorf("PersonaID mismatch: %q", tc.PersonaID)
	}
}
