package main

import (
	"reflect"
	"sync"
	"testing"
)

func TestModels(t *testing.T) {
	want := []string{
		"shortlink code=b old=old err=<nil>",
		"sale first=true replay=true other=false stock=0",
		"feed=[9 7 4]",
		"chat seq=1 replay=1 missed=1",
		"limit=true true false true",
		"ids distinct=true rollback=clock moved backward",
		"job old_ack=false current_ack=true",
		"ranking=[{b 100} {a 90} {z 90}]",
		"counter dedup=1 merged=5",
		"suggest=[go golang]",
		"payment state=succeeded entries=2 balance=0",
		"notify attempts=2 visible=1",
	}
	if got := demos(); !reflect.DeepEqual(got, want) {
		t.Fatalf("got %q\nwant %q", got, want)
	}
}
func TestConcurrentSale(t *testing.T) {
	s := &Sale{Stock: 3, Orders: map[string]bool{}}
	var wg sync.WaitGroup
	for i := range 100 {
		wg.Go(func() { s.Reserve(string(rune(i))) })
	}
	wg.Wait()
	accepted := 0
	for _, ok := range s.Orders {
		if ok {
			accepted++
		}
	}
	if s.Stock != 0 || accepted != 3 {
		t.Fatal(s.Stock, accepted)
	}
}
func TestFailures(t *testing.T) {
	t.Run("shortlink exhaustion preserves data", func(t *testing.T) {
		m := map[string]string{"a": "old"}
		if _, err := allocate(m, "new", []string{"a"}); err == nil || m["a"] != "old" {
			t.Fatal(m, err)
		}
	})
	t.Run("chat same key different body", func(t *testing.T) {
		c := &Chat{Seen: map[string]Message{}}
		_, _ = c.Send("k", "one")
		if _, err := c.Send("k", "two"); err == nil {
			t.Fatal("accepted changed payload")
		}
	})
	t.Run("bucket clock rollback", func(t *testing.T) {
		b := &Bucket{Tokens: 1, Capacity: 1, Rate: 1, LastMS: 10}
		if _, err := b.Allow(9); err == nil {
			t.Fatal("clock rollback")
		}
	})
	t.Run("ID exhaustion", func(t *testing.T) {
		g := &IDGen{}
		for range 4096 {
			if _, err := g.Next(10); err != nil {
				t.Fatal(err)
			}
		}
		if _, err := g.Next(10); err == nil {
			t.Fatal("wrapped sequence")
		}
		if _, err := g.Next(11); err != nil {
			t.Fatal(err)
		}
	})
	t.Run("job not due", func(t *testing.T) {
		j := &Job{Due: 10}
		if _, ok := j.Claim(9, 5); ok {
			t.Fatal("early")
		}
		if _, ok := j.Claim(10, 5); !ok {
			t.Fatal("not claimed")
		}
		if _, ok := j.Claim(11, 5); ok {
			t.Fatal("duplicate lease")
		}
	})
	t.Run("payment mismatch", func(t *testing.T) {
		l := &Ledger{Payments: map[string]Payment{"p": {Amount: 100, Currency: "USD"}}, Posted: map[string]bool{}}
		if err := l.Reconcile("p", "fake", "EUR", 100); err == nil || len(l.Entries) > 0 {
			t.Fatal("posted mismatch")
		}
	})
	t.Run("payment wrong amount leaves state unchanged", func(t *testing.T) {
		original := Payment{State: "unknown", Amount: 100, Currency: "USD"}
		l := &Ledger{Payments: map[string]Payment{"p": original}, Posted: map[string]bool{}}
		if err := l.Reconcile("p", "fake", "USD", 101); err == nil {
			t.Fatal("accepted wrong amount")
		}
		if l.Payments["p"] != original || len(l.Entries) != 0 || len(l.Posted) != 0 {
			t.Fatal("rejected observation mutated state")
		}
	})
	t.Run("payment provider conflict after success", func(t *testing.T) {
		l := &Ledger{Payments: map[string]Payment{"p": {State: "unknown", Amount: 100, Currency: "USD"}}, Posted: map[string]bool{}}
		if err := l.Reconcile("p", "fake-1", "USD", 100); err != nil {
			t.Fatal(err)
		}
		original := l.Payments["p"]
		if err := l.Reconcile("p", "fake-2", "USD", 100); err == nil {
			t.Fatal("accepted another provider ID")
		}
		if l.Payments["p"] != original || len(l.Entries) != 2 || balance(l.Entries) != 0 {
			t.Fatal("provider conflict mutated state")
		}
		if err := l.Reconcile("p", "fake-1", "USD", 100); err != nil || len(l.Entries) != 2 {
			t.Fatal("duplicate posted", err)
		}
	})
	t.Run("G-counter merge repeat", func(t *testing.T) {
		a, b := map[string]int{"a": 2}, map[string]int{"b": 3}
		m := mergeG(a, b)
		if sum(mergeG(m, b)) != 5 {
			t.Fatal("non-idempotent merge")
		}
	})
}

// Controlled counterexample: no data race, yet a read-check-write schedule oversells.
func TestCheckThenWriteCounterexample(t *testing.T) {
	stock := 1
	aSaw, bSaw := stock, stock
	accepted := 0
	if aSaw > 0 {
		stock = aSaw - 1
		accepted++
	}
	if bSaw > 0 {
		stock = bSaw - 1
		accepted++
	}
	if stock != 0 || accepted != 2 {
		t.Fatal(stock, accepted)
	}
	t.Logf("broken schedule: stock=%d accepted=%d", stock, accepted)
}
