package spindle

import (
	"testing"
	"time"
)

type slotClock struct{ at time.Time }

func (c *slotClock) now() time.Time { return c.at }

func newSlots() (*notifySlots, *slotClock) {
	clock := &slotClock{at: time.Unix(1000, 0)}
	return &notifySlots{now: clock.now}, clock
}

func shared(pool int) slotLimits { return slotLimits{pool: pool, reserve: slotReserve(pool)} }

func take(n *notifySlots, host string, lim slotLimits, count int) (got int) {
	for range count {
		if n.acquire(host, lim) {
			got++
		}
	}
	return got
}

func fill(t *testing.T, n *notifySlots, lim slotLimits) {
	t.Helper()
	if got := take(n, "hog.test", lim, lim.pool); got != lim.pool-lim.reserve {
		t.Fatalf("setup: the hog got %d of %d, want all but the reserve %d", got, lim.pool, lim.reserve)
	}
	if got := take(n, "second.test", lim, lim.reserve); got != lim.reserve {
		t.Fatalf("setup: the second knot got %d of the reserve of %d", got, lim.reserve)
	}
}

func TestNotifySlotsAnIdlePoolKeepsAReserveForANewcomer(t *testing.T) {
	for pool, reserve := range map[int]int{64: 8, 16: 2, 8: 2, 3: 2} {
		n, _ := newSlots()
		lim := shared(pool)
		if lim.reserve != reserve {
			t.Fatalf("reserve of a pool of %d = %d, want %d", pool, lim.reserve, reserve)
		}
		got := take(n, "hog.test", lim, pool)
		if want := max(1, pool-reserve); got != want {
			t.Errorf("pool of %d: the hog got %d, want %d", pool, got, want)
		}
		if !n.acquire("newcomer.test", lim) {
			t.Errorf("pool of %d: a newcomer found no slot on an idle pool", pool)
		}
	}
}

func TestNotifySlotsKnotsThatWaitGetAnEqualShare(t *testing.T) {
	n, _ := newSlots()
	lim := shared(16)
	fill(t, n, lim)
	take(n, "b.test", lim, 1)
	take(n, "c.test", lim, 1)

	for range 4 {
		n.release("hog.test")
	}
	if n.acquire("hog.test", lim) {
		t.Fatal("the hog took a freed slot back while two knots waited")
	}
	for _, host := range []string{"b.test", "c.test", "b.test", "c.test"} {
		if !n.acquire(host, lim) {
			t.Fatalf("%s was refused a slot inside its share", host)
		}
	}
	if n.held["b.test"] != 2 || n.held["c.test"] != 2 {
		t.Fatalf("held = %v, want the freed slots split between the knots that waited", n.held)
	}
}

func TestNotifySlotsAHostileKnotCantGrowPastItsShareWhileAnotherHasWork(t *testing.T) {
	n, _ := newSlots()
	lim := shared(16)
	fill(t, n, lim)
	if n.acquire("honest.test", lim) {
		t.Fatal("the pool was full, so the honest knot should wait")
	}

	n.release("hog.test")
	if n.acquire("hog.test", lim) {
		t.Fatalf("the hostile knot took a freed slot back while holding %d and another knot waited", n.held["hog.test"])
	}
	if !n.acquire("honest.test", lim) {
		t.Fatal("the slot the hostile knot let go of went to nobody")
	}
}

func TestNotifySlotsAKnotThatStopsWaitingStopsCounting(t *testing.T) {
	n, clock := newSlots()
	lim := shared(8)
	fill(t, n, lim)
	n.acquire("b.test", lim)

	clock.at = clock.at.Add(2 * notifyWaitWindow)
	n.release("second.test")
	n.release("second.test")
	n.release("hog.test")
	n.release("hog.test")
	if got := take(n, "hog.test", lim, 2); got != 2 {
		t.Fatalf("the hog got %d slots after b stopped waiting, want both", got)
	}
}

func TestNotifySlotsAKnotBackingOffStillLimitsTheHog(t *testing.T) {
	n, clock := newSlots()
	lim := shared(16)
	fill(t, n, lim)
	if n.acquire("legit.test", lim) {
		t.Fatal("the pool was full, so the legit knot should wait")
	}

	clock.at = clock.at.Add(30 * time.Second)
	for range 6 {
		n.release("hog.test")
	}
	if n.acquire("hog.test", lim) {
		t.Fatalf("the hog took a slot back at %d held while the legit knot was only backing off", n.held["hog.test"])
	}
	clock.at = clock.at.Add(30 * time.Second)
	if !n.acquire("legit.test", lim) {
		t.Fatal("the legit knot found no slot when its retry came")
	}
}

func TestNotifySlotsAPriorityKnotGetsInWhileAHostileKnotPinsThePool(t *testing.T) {
	n, _ := newSlots()
	lim := shared(16)
	lim.dedicated = 4
	lim.priority = priorityKnotHosts([]string{"prio.test"})
	fill(t, n, lim)

	if got := take(n, "prio.test", lim, 4); got != 4 {
		t.Fatalf("the priority knot got %d of its 4 dedicated slots with the shared pool full", got)
	}
	if n.acquire("prio.test", lim) {
		t.Fatal("a fifth slot came from a full shared pool")
	}
	if n.prioTotal != 4 || n.total != 16 {
		t.Fatalf("dedicated %d, shared %d, want 4 and 16", n.prioTotal, n.total)
	}
}

func TestNotifySlotsANonPriorityKnotCantUseTheDedicatedSlots(t *testing.T) {
	n, _ := newSlots()
	lim := shared(16)
	lim.dedicated = 4
	lim.priority = priorityKnotHosts([]string{"prio.test"})
	fill(t, n, lim)

	if n.acquire("plain.test", lim) {
		t.Fatal("a knot that isn't priority took a slot while the shared pool was full")
	}
	if n.prioTotal != 0 {
		t.Fatalf("%d dedicated slots in use, want none", n.prioTotal)
	}
}

func TestNotifySlotsAPriorityKnotFallsBackToTheSharedPoolUnderTheNormalShare(t *testing.T) {
	n, _ := newSlots()
	lim := shared(16)
	lim.dedicated = 2
	lim.priority = priorityKnotHosts([]string{"prio.test"})

	if got := take(n, "prio.test", lim, 20); got != 2+14 {
		t.Fatalf("the priority knot got %d, want its 2 dedicated and all but the reserve of the shared pool", got)
	}
	n.release("prio.test")
	if n.held["prio.test"] != 13 || n.prioTotal != 2 {
		t.Fatalf("shared held %d, dedicated %d after one release, want 13 and 2", n.held["prio.test"], n.prioTotal)
	}
}

func TestPriorityKnotsMatchByHostOrDid(t *testing.T) {
	hosts := priorityKnotHosts([]string{"Knot1.Example.com", "did:web:knot2.example.com", " did:web:Knot3.Example.com%3A8443 ", ""})
	for _, host := range []string{"knot1.example.com", "KNOT1.example.com.", "knot2.example.com", "Knot3.example.com:8443"} {
		if !hosts[canonicalKnotHost(host)] {
			t.Errorf("%s isn't a priority knot, want it matched by host and by did", host)
		}
	}
	if hosts[canonicalKnotHost("knot3.example.com")] || hosts[canonicalKnotHost("other.example.com")] || len(hosts) != 3 {
		t.Fatalf("hosts = %v, want three, and the port to count", hosts)
	}
}

func TestNotifySlotsAreSizedFromTheConfig(t *testing.T) {
	h := newHookEnv(t)
	if got := h.s.notifySlotCount(); got != defaultNotifySlots {
		t.Fatalf("pool = %d with nothing configured, want %d", got, defaultNotifySlots)
	}
	h.s.cfg.Server.NotifySlots = 5
	if got := h.s.notifySlotCount(); got != 5 {
		t.Fatalf("pool = %d, want the configured 5", got)
	}
	var release []func()
	for _, host := range []string{"a.test", "b.test", "c.test", "d.test", "e.test"} {
		r, ok := h.s.acquireNotifySlot(host)
		if !ok {
			t.Fatalf("slot refused for %s below the configured pool", host)
		}
		release = append(release, r)
	}
	if _, ok := h.s.acquireNotifySlot("f.test"); ok {
		t.Fatal("a sixth slot was granted from a pool of 5")
	}
	for _, r := range release {
		r()
	}
	if got := h.s.notifySlots.inflight(); got != 0 {
		t.Fatalf("%d slots held after releasing all", got)
	}
}

func TestPriorityNotifySlotsAreSizedFromTheConfig(t *testing.T) {
	h := newHookEnv(t)
	h.s.cfg.Server.PriorityKnots = []string{"did:web:Prio.Test"}

	lim := h.s.notifyLimits()
	if lim.dedicated != defaultNotifySlots/4 || !lim.priority["prio.test"] {
		t.Fatalf("limits = %+v, want a quarter of the pool dedicated and prio.test priority", lim)
	}
	h.s.cfg.Server.PriorityNotifySlots = 3
	if got := h.s.notifyLimits().dedicated; got != 3 {
		t.Fatalf("dedicated = %d, want the configured 3", got)
	}
}

func TestNotifyCapsKeyOnOneSpellingOfAKnotHost(t *testing.T) {
	n, _ := newSlots()
	for _, host := range []string{"Knot.Test", "knot.test.", "KNOT.test"} {
		if !n.acquire(host, shared(8)) {
			t.Fatalf("slot refused for %s below the pool", host)
		}
	}
	if len(n.held) != 1 || n.held["knot.test"] != 3 {
		t.Fatalf("held = %v, want one knot holding 3", n.held)
	}
	for _, host := range []string{"knot.test", "KNOT.test.", "Knot.Test"} {
		n.release(host)
	}
	if len(n.held) != 0 || n.inflight() != 0 {
		t.Fatalf("held = %v after releasing under other spellings", n.held)
	}

	var unknown notifyAdmission
	if !unknown.acquire("Knot.Test") || unknown.acquire("knot.test.") {
		t.Fatal("the unknown-repo pool keyed one knot under two spellings")
	}
}

func TestCanonicalKnotHostKeepsThePort(t *testing.T) {
	for raw, want := range map[string]string{
		"Knot.Test:8443": "knot.test:8443",
		"knot.test.":     "knot.test",
		"knot.test":      "knot.test",
	} {
		if got := canonicalKnotHost(raw); got != want {
			t.Errorf("canonicalKnotHost(%q) = %q, want %q", raw, got, want)
		}
	}
}
