package main

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/zenmoney"
)

func TestMaxSeverity(t *testing.T) {
	parse := func(s string) netdataAlarms {
		var a netdataAlarms
		if err := json.Unmarshal([]byte(s), &a); err != nil {
			t.Fatal(err)
		}
		return a
	}
	cases := []struct{ body, want string }{
		{`{"alarms":{}}`, "clear"},
		{`{"alarms":{"a":{"status":"WARNING"}}}`, "warning"},
		{`{"alarms":{"a":{"status":"WARNING"},"b":{"status":"CRITICAL"}}}`, "critical"},
		{`{"alarms":{"a":{"status":"CLEAR"}}}`, "clear"},
	}
	for _, c := range cases {
		if got := maxSeverity(parse(c.body)); got != c.want {
			t.Errorf("maxSeverity(%s) = %q, want %q", c.body, got, c.want)
		}
	}
}

func TestKumaMonitorsNamesEveryOne(t *testing.T) {
	cases := []struct {
		name string
		body string
		want map[string]string
	}{
		{"empty body", "", map[string]string{}},
		{"help line only", `# HELP monitor_status ...`, map[string]string{}},
		{
			"one up one down",
			`monitor_status{monitor_name="web"} 1` + "\n" + `monitor_status{monitor_name="db"} 0`,
			map[string]string{"web": "up", "db": "down"},
		},
		{"maintenance is not down", `monitor_status{monitor_name="mnt"} 3`, map[string]string{"mnt": "maintenance"}},
		{"pending is not down", `monitor_status{monitor_name="p"} 2`, map[string]string{"p": "pending"}},
		{
			"other labels do not hide the name",
			`monitor_status{monitor_type="http",monitor_name="ci",monitor_url="x"} 0`,
			map[string]string{"ci": "down"},
		},
		{"a nameless line is skipped", `monitor_status{monitor_type="http"} 0`, map[string]string{}},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := kumaMonitors([]byte(c.body))
			if len(got) != len(c.want) {
				t.Fatalf("kumaMonitors = %v, want %v", got, c.want)
			}
			for k, v := range c.want {
				if got[k] != v {
					t.Errorf("monitor %q = %q, want %q", k, got[k], v)
				}
			}
		})
	}
}

func TestKumaFactKeyCarriesTheName(t *testing.T) {
	if got := kumaFactKey("nexus db"); got != "service_down:nexus db" {
		t.Errorf("kumaFactKey = %q", got)
	}
}

func TestParseMaxHandshake(t *testing.T) {
	cases := []struct {
		out  string
		want int64
	}{
		{"", 0},
		{"pubkeyAAA\t0\n", 0}, // never handshaked
		{"pubkeyAAA\t1700000000\n", 1700000000},
		{"pubkeyAAA\t1700000000\npubkeyBBB\t1700000500\n", 1700000500},  // max wins
		{"wg0\tpubkeyAAA\t1700000000\nwg0\tpubkeyBBB\t0\n", 1700000000}, // 'all' 3-field form
	}
	for _, c := range cases {
		if got := parseMaxHandshake(c.out); got != c.want {
			t.Errorf("parseMaxHandshake(%q) = %d, want %d", c.out, got, c.want)
		}
	}
}

// ---- zenmoney (Vikunja #125) ----------------------------------------------

// factCore records the facts the poller wrote and answers "no fact yet".
type factCore struct {
	ipc.UnimplementedCoreAPI

	written []ipc.WriteFactReq
	prev    map[string]string
}

func (c *factCore) LatestFactBySource(_ context.Context, key, source string) (ipc.Fact, error) {
	if v, ok := c.prev[key+"|"+source]; ok {
		return ipc.Fact{Key: key, Source: source, Value: v}, nil
	}
	return ipc.Fact{}, ipc.ErrNoFact
}

func (c *factCore) WriteFact(_ context.Context, req ipc.WriteFactReq) (int64, error) {
	c.written = append(c.written, req)
	return int64(len(c.written)), nil
}

func zenFixtureServer(t *testing.T, body []byte, status int) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if status != http.StatusOK {
			w.WriteHeader(status)
			return
		}
		w.Write(body)
	}))
}

func TestPollZenmoneyWritesMoneyFacts(t *testing.T) {
	body, err := os.ReadFile("../../internal/zenmoney/testdata/diff.json")
	if err != nil {
		t.Fatal(err)
	}
	srv := zenFixtureServer(t, body, http.StatusOK)
	defer srv.Close()
	zen, err := zenmoney.New("tok", srv.URL, time.Second)
	if err != nil {
		t.Fatal(err)
	}
	core := &factCore{}
	p := &poller{core: core, zen: zen}
	now := time.Date(2026, 8, 1, 21, 0, 0, 0, time.UTC)
	if err := p.pollZenmoney(context.Background(), now); err != nil {
		t.Fatal(err)
	}
	if len(core.written) != 2 {
		t.Fatalf("wrote %d facts, want today + month", len(core.written))
	}
	for _, f := range core.written {
		if f.Kind != "env" || f.Source != zenmoney.Source {
			t.Errorf("fact = %+v, want kind=env source=%s", f, zenmoney.Source)
		}
		if _, err := zenmoney.ParseFactValue(f.Value); err != nil {
			t.Errorf("fact value %q does not decode: %v", f.Value, err)
		}
	}
}

// A read that returns nothing for the window writes NOTHING. Silence, not a
// zero: an invented 0 would be recited back to him as fact.
func TestPollZenmoneyWritesNothingWhenEmpty(t *testing.T) {
	srv := zenFixtureServer(t, []byte(`{"serverTimestamp":1,"instrument":[],"transaction":[]}`), http.StatusOK)
	defer srv.Close()
	zen, _ := zenmoney.New("tok", srv.URL, time.Second)
	core := &factCore{}
	p := &poller{core: core, zen: zen}
	if err := p.pollZenmoney(context.Background(), time.Now()); err != nil {
		t.Fatal(err)
	}
	if len(core.written) != 0 {
		t.Errorf("wrote %+v, want no fact at all", core.written)
	}
}

// An API failure must not overwrite the last good total either.
func TestPollZenmoneyFailureWritesNothing(t *testing.T) {
	srv := zenFixtureServer(t, nil, http.StatusUnauthorized)
	defer srv.Close()
	zen, _ := zenmoney.New("bad", srv.URL, time.Second)
	core := &factCore{}
	p := &poller{core: core, zen: zen}
	if err := p.pollZenmoney(context.Background(), time.Now()); err == nil {
		t.Error("want the 401 reported")
	}
	if len(core.written) != 0 {
		t.Errorf("wrote %+v on a failed read", core.written)
	}
}

// Unchanged totals do not churn the facts table.
func TestWriteIfChangedRawSkipsUnchanged(t *testing.T) {
	core := &factCore{prev: map[string]string{
		zenmoney.KeySpentToday + "|" + zenmoney.Source: `{"count":1}`,
	}}
	p := &poller{core: core}
	if err := p.writeIfChangedRaw(context.Background(), zenmoney.KeySpentToday, zenmoney.Source, `{"count":1}`, time.Now()); err != nil {
		t.Fatal(err)
	}
	if len(core.written) != 0 {
		t.Errorf("wrote %+v for an unchanged value", core.written)
	}
}

// Money tracking is off unless configured: no token file, no zenmoney client,
// and the poller still refuses to start with nothing at all to poll.
func TestRunRequiresSomethingToPoll(t *testing.T) {
	err := run([]string{"-socket", "/tmp/nope.sock", "-netdata", "", "-kuma", "", "-wg", ""})
	if err == nil || !strings.Contains(err.Error(), "nothing to poll") {
		t.Errorf("err = %v, want a 'nothing to poll' refusal", err)
	}
}
