package spindle

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"
)

func strangers(n int) []syntax.DID {
	var out []syntax.DID
	for i := range n {
		out = append(out, syntax.DID(fmt.Sprintf("did:plc:stranger%d", i)))
	}
	return out
}

func TestNotifyEventUnknownReposDontTakeKnownReposSlots(t *testing.T) {
	e := newPushEnv(t)
	release := e.w.holdManifest()
	defer release(false)

	held := strangers(8)
	var answered atomic.Int32
	var wg sync.WaitGroup
	for _, did := range held {
		wg.Go(func() {
			e.n.s.NotifyEvent(context.Background(), e.w.knotRef(), did, faultPush("refs/heads/main", zeroSha, strings.Repeat("a", 40)))
			answered.Add(1)
		})
	}
	eventually(t, 10*time.Second, "the strangers to be held on the knot or turned away", func() bool {
		return int(e.w.heldReads.Load()+answered.Load()) == len(held)
	})

	uri, err := e.notify(e.n.s, e.firstPush(t))
	if err != nil || uri == "" {
		t.Fatalf("the served repo's push answered %q, %v while strangers were held, want a pipeline", uri, err)
	}
	release(false)
	wg.Wait()
}

func TestNotifyEventRemembersReposItDoesNotServe(t *testing.T) {
	e := newPushEnv(t)
	stranger := syntax.DID("did:plc:stranger")
	push := faultPush("refs/heads/main", zeroSha, strings.Repeat("a", 40))

	if uri, err := e.n.s.NotifyEvent(context.Background(), e.w.knotRef(), stranger, push); err != nil || uri != "" {
		t.Fatalf("first notify answered %q, %v, want an empty answer", uri, err)
	}
	before := e.w.traffic()
	if uri, err := e.n.s.NotifyEvent(context.Background(), e.w.knotRef(), stranger, push); err != nil || uri != "" {
		t.Fatalf("second notify answered %q, %v, want an empty answer", uri, err)
	}
	if got := e.w.traffic() - before; got != 0 {
		t.Fatalf("second notify of an unknown repo made %d knot requests, want 0", got)
	}
}

func TestNotifyAdmissionStillRemembersAfterAFloodOfDistinctDids(t *testing.T) {
	var a notifyAdmission
	for i := range unknownRepoMemoryMax + 10 {
		a.miss(syntax.DID(fmt.Sprintf("did:plc:flood%d", i)))
	}

	last := syntax.DID("did:plc:flood" + fmt.Sprint(unknownRepoMemoryMax+9))
	if !a.missedRecently(last) {
		t.Fatal("a miss after the memory filled was forgotten, so real misses cost a read again")
	}
	if a.missedRecently("did:plc:flood0") {
		t.Fatal("the oldest miss outlived the cap")
	}
}
