package main

import (
	"encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/anthropics/anthropic-sdk-go"
)

func TestTimeContextFor(t *testing.T) {
	cases := []struct {
		hour     int
		expected string
	}{
		{3, "night"},
		{5, "morning"},
		{11, "morning"},
		{12, "afternoon"},
		{17, "afternoon"},
		{18, "evening"},
		{21, "evening"},
		{22, "night"},
		{23, "night"},
	}
	for _, tc := range cases {
		ts := int(time.Date(2025, 5, 15, tc.hour, 0, 0, 0, time.Local).Unix())
		if got := timeContextFor(ts); got != tc.expected {
			t.Errorf("timeContextFor(hour=%d) = %q, want %q", tc.hour, got, tc.expected)
		}
	}
}

func TestBuildUserContext(t *testing.T) {
	noon := int(time.Date(2025, 5, 15, 12, 0, 0, 0, time.Local).Unix())

	got := buildUserContext("alice", "Alice", "Smith", true, "de", noon)
	for _, want := range []string{"Alice Smith", "@alice", "Preferred language: de", "premium user", "afternoon"} {
		if !strings.Contains(got, want) {
			t.Errorf("buildUserContext premium: missing %q in:\n%s", want, got)
		}
	}

	got = buildUserContext("", "", "", false, "", noon)
	for _, want := range []string{"User: unknown (Telegram @unknown)", "Preferred language: en", "regular user"} {
		if !strings.Contains(got, want) {
			t.Errorf("buildUserContext fallback: missing %q in:\n%s", want, got)
		}
	}

	got = buildUserContext("bob", "Bob", "", false, "en", noon)
	if !strings.Contains(got, "User: Bob (Telegram @bob)") {
		t.Errorf("buildUserContext firstname-only: got:\n%s", got)
	}
}

func TestThinkingParamFromConfig(t *testing.T) {
	cases := []struct {
		name    string
		mode    string
		display string
		ok      bool
		want    map[string]any
	}{
		{"unset omits param", "", "", false, nil},
		{"unknown value omits param", "bogus", "", false, nil},
		{"adaptive no display", ThinkingModeAdaptive, "", true,
			map[string]any{"type": "adaptive"}},
		{"adaptive summarized", ThinkingModeAdaptive, ThinkingDisplaySummarized, true,
			map[string]any{"type": "adaptive", "display": "summarized"}},
		{"adaptive omitted", ThinkingModeAdaptive, ThinkingDisplayOmitted, true,
			map[string]any{"type": "adaptive", "display": "omitted"}},
		{"disabled", ThinkingModeDisabled, "", true,
			map[string]any{"type": "disabled"}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			union, ok := thinkingParamFromConfig(tc.mode, tc.display)
			if ok != tc.ok {
				t.Fatalf("ok = %v, want %v", ok, tc.ok)
			}
			if !tc.ok {
				return
			}
			raw, err := json.Marshal(union)
			if err != nil {
				t.Fatalf("marshal: %v", err)
			}
			var got map[string]any
			if err := json.Unmarshal(raw, &got); err != nil {
				t.Fatalf("unmarshal %s: %v", raw, err)
			}
			if len(got) != len(tc.want) {
				t.Fatalf("wire shape %s: got %d keys, want %d (%v)", raw, len(got), len(tc.want), tc.want)
			}
			for k, v := range tc.want {
				if got[k] != v {
					t.Errorf("wire shape %s: key %q = %v, want %v", raw, k, got[k], v)
				}
			}
		})
	}
}

func TestBackwardCompatibleParams(t *testing.T) {
	params := anthropic.BetaMessageNewParams{
		Model:     "claude-test",
		MaxTokens: defaultMaxTokens,
		Messages: []anthropic.BetaMessageParam{
			anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("hi")),
		},
	}
	raw, err := json.Marshal(params)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var got map[string]any
	if err := json.Unmarshal(raw, &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if _, present := got["thinking"]; present {
		t.Errorf("zero Thinking union must omit the key; body: %s", raw)
	}
	if mt, ok := got["max_tokens"].(float64); !ok || int(mt) != defaultMaxTokens {
		t.Errorf("max_tokens = %v, want %d; body: %s", got["max_tokens"], defaultMaxTokens, raw)
	}
}

func TestWebSearchTools(t *testing.T) {
	t.Run("nil config yields no tools", func(t *testing.T) {
		if tools := webSearchTools(nil); tools != nil {
			t.Errorf("webSearchTools(nil) = %v, want nil", tools)
		}
	})

	t.Run("search only when fetch off", func(t *testing.T) {
		tools := webSearchTools(&WebSearchConfig{
			AllowedDomains: []string{"example.com/hc"},
			MaxUses:        3,
		})
		if len(tools) != 1 {
			t.Fatalf("got %d tools, want 1 (search only)", len(tools))
		}
		if tools[0].OfWebSearchTool20250305 == nil {
			t.Fatalf("tools[0] is not a web_search tool")
		}
		if tools[0].OfWebFetchTool20250910 != nil {
			t.Error("web_fetch tool present but fetch is off")
		}
	})

	t.Run("search + fetch with allowlist and citations", func(t *testing.T) {
		tools := webSearchTools(&WebSearchConfig{
			AllowedDomains:   []string{"example.com/hc", "docs.example.com"},
			MaxUses:          3,
			Fetch:            true,
			MaxContentTokens: 50000,
		})
		if len(tools) != 2 {
			t.Fatalf("got %d tools, want 2 (search + fetch)", len(tools))
		}

		search := tools[0].OfWebSearchTool20250305
		if search == nil {
			t.Fatalf("tools[0] is not a web_search tool")
		}
		if !sameStrings(search.AllowedDomains, []string{"example.com/hc", "docs.example.com"}) {
			t.Errorf("search AllowedDomains = %v, want the path-scoped list unchanged", search.AllowedDomains)
		}
		if search.MaxUses.Value != 3 {
			t.Errorf("search MaxUses = %d, want 3", search.MaxUses.Value)
		}

		fetch := tools[1].OfWebFetchTool20250910
		if fetch == nil {
			t.Fatalf("tools[1] is not a web_fetch tool")
		}
		if !sameStrings(fetch.AllowedDomains, []string{"example.com", "docs.example.com"}) {
			t.Errorf("fetch AllowedDomains = %v, want host-only [example.com docs.example.com]", fetch.AllowedDomains)
		}
		if fetch.MaxContentTokens.Value != 50000 {
			t.Errorf("fetch MaxContentTokens = %d, want 50000", fetch.MaxContentTokens.Value)
		}
		if fetch.Citations.Enabled.Value != true {
			t.Error("fetch citations not enabled")
		}
	})

	t.Run("fetch hosts are deduped", func(t *testing.T) {
		tools := webSearchTools(&WebSearchConfig{
			AllowedDomains: []string{"a.com/x", "a.com/y", "b.com"},
			Fetch:          true,
		})
		fetch := tools[1].OfWebFetchTool20250910
		if fetch == nil {
			t.Fatalf("tools[1] is not a web_fetch tool")
		}
		if !sameStrings(fetch.AllowedDomains, []string{"a.com", "b.com"}) {
			t.Errorf("fetch AllowedDomains = %v, want deduped [a.com b.com]", fetch.AllowedDomains)
		}
	})

	t.Run("social host is search-only via fetch_allowed_domains", func(t *testing.T) {
		tools := webSearchTools(&WebSearchConfig{
			AllowedDomains:      []string{"helpshift.example/hc", "x.com/thatskygame"},
			FetchAllowedDomains: []string{"helpshift.example"},
			Fetch:               true,
		})
		search := tools[0].OfWebSearchTool20250305
		if search == nil {
			t.Fatalf("tools[0] is not a web_search tool")
		}
		var searchHasSocial bool
		for _, d := range search.AllowedDomains {
			if d == "x.com/thatskygame" {
				searchHasSocial = true
			}
		}
		if !searchHasSocial {
			t.Errorf("search AllowedDomains = %v, want it to include x.com/thatskygame", search.AllowedDomains)
		}

		fetch := tools[1].OfWebFetchTool20250910
		if fetch == nil {
			t.Fatalf("tools[1] is not a web_fetch tool")
		}
		if !sameStrings(fetch.AllowedDomains, []string{"helpshift.example"}) {
			t.Errorf("fetch AllowedDomains = %v, want only [helpshift.example]", fetch.AllowedDomains)
		}
		for _, d := range fetch.AllowedDomains {
			if strings.HasPrefix(d, "x.com") {
				t.Errorf("fetch AllowedDomains leaked the social host: %v", fetch.AllowedDomains)
			}
		}
	})

	t.Run("wire shape carries allowed_domains", func(t *testing.T) {
		tools := webSearchTools(&WebSearchConfig{
			AllowedDomains: []string{"thatgamecompany.helpshift.com/hc"},
			MaxUses:        2,
			Fetch:          true,
		})
		raw, err := json.Marshal(tools)
		if err != nil {
			t.Fatalf("marshal: %v", err)
		}
		body := string(raw)
		for _, want := range []string{
			"web_search_20250305",
			"web_fetch_20250910",
			"thatgamecompany.helpshift.com/hc",
			"allowed_domains",
		} {
			if !strings.Contains(body, want) {
				t.Errorf("wire body missing %q:\n%s", want, body)
			}
		}
	})
}

func sameStrings(got, want []string) bool {
	if len(got) != len(want) {
		return false
	}
	for i := range got {
		if got[i] != want[i] {
			return false
		}
	}
	return true
}

func TestFetchHosts(t *testing.T) {
	cases := []struct {
		name string
		in   []string
		want []string
	}{
		{"nil in nil out", nil, nil},
		{"empty in nil out", []string{}, nil},
		{"host passthrough", []string{"example.com"}, []string{"example.com"}},
		{"strip path", []string{"example.com/hc/en"}, []string{"example.com"}},
		{"dedup after strip", []string{"a.com/x", "a.com/y"}, []string{"a.com"}},
		{"preserve order and subdomains", []string{"docs.example.com/a", "example.com"}, []string{"docs.example.com", "example.com"}},
		{"drop empty leading slash", []string{"/oops", "ok.com"}, []string{"ok.com"}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := fetchHosts(tc.in); !sameStrings(got, tc.want) {
				t.Errorf("fetchHosts(%v) = %v, want %v", tc.in, got, tc.want)
			}
		})
	}
}

func TestEmptyStreamError(t *testing.T) {
	err := emptyStreamError("max_tokens", 3900, 4000)
	for _, want := range []string{"output budget exhausted", "3900", "4000"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("max_tokens case: %q missing %q", err.Error(), want)
		}
	}
	if got := emptyStreamError("end_turn", 0, 1000).Error(); got != "unexpected response format from Anthropic" {
		t.Errorf("generic case = %q", got)
	}
	if got := emptyStreamError("", 0, 1000).Error(); got != "unexpected response format from Anthropic" {
		t.Errorf("no-stop-reason case = %q", got)
	}
}
