package wgsync

// Group D — reconciler (Scenario D). Non-hollow: asserts the exact pushed payloads (full list,
// deterministic order, absent-after-remove), retry-after-failure, and the no-mutation drift-
// repair tick.

import (
	"context"
	"encoding/json"
	"io"
	"log"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

type fakeSyncer struct {
	mu       sync.Mutex
	payloads [][]byte
	err      error
	notify   chan struct{}
}

func newFakeSyncer() *fakeSyncer { return &fakeSyncer{notify: make(chan struct{}, 16)} }

func (f *fakeSyncer) Push(ctx context.Context, payload []byte) error {
	f.mu.Lock()
	cp := append([]byte(nil), payload...)
	f.payloads = append(f.payloads, cp)
	err := f.err
	f.mu.Unlock()
	select {
	case f.notify <- struct{}{}:
	default:
	}
	return err
}

func (f *fakeSyncer) count() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return len(f.payloads)
}

func (f *fakeSyncer) last() []byte {
	f.mu.Lock()
	defer f.mu.Unlock()
	if len(f.payloads) == 0 {
		return nil
	}
	return f.payloads[len(f.payloads)-1]
}

func waitPush(t *testing.T, f *fakeSyncer) {
	t.Helper()
	select {
	case <-f.notify:
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for a push")
	}
}

func newWGStore(t *testing.T) *store.Store {
	t.Helper()
	s, err := store.New(filepath.Join(t.TempDir(), "test.db"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatalf("store.New: %v", err)
	}
	t.Cleanup(func() { s.Close() })
	if err := s.SetWGEndpoint(&store.WGEndpoint{
		DNSName: "ep0.example", WGPort: 443, ServerPubkey: "SPK",
		TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1",
	}); err != nil {
		t.Fatalf("SetWGEndpoint: %v", err)
	}
	return s
}

func TestReconciler_TriggerPushesFullList(t *testing.T) {
	st := newWGStore(t)
	st.AddWGPeer("PKA", "", "")
	st.AddWGPeer("PKB", "", "")
	fake := newFakeSyncer()
	r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
	r.interval = time.Hour // tick out of the picture

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go r.Run(ctx)

	r.Trigger()
	waitPush(t, fake)
	if fake.count() != 1 {
		t.Fatalf("push count = %d, want exactly 1", fake.count())
	}
	var doc struct {
		Version   int    `json:"version"`
		Interface string `json:"interface"`
		Peers     []struct {
			Pubkey    string `json:"pubkey"`
			AllowedIP string `json:"allowed_ip"`
		} `json:"peers"`
	}
	if err := json.Unmarshal(fake.last(), &doc); err != nil {
		t.Fatalf("payload not JSON: %v", err)
	}
	if doc.Version != 1 || doc.Interface != "wg0" || len(doc.Peers) != 2 {
		t.Fatalf("payload = %s", fake.last())
	}
	if doc.Peers[0].Pubkey != "PKA" || doc.Peers[0].AllowedIP != "10.77.0.2/32" ||
		doc.Peers[1].Pubkey != "PKB" || doc.Peers[1].AllowedIP != "10.77.0.3/32" {
		t.Errorf("peers = %+v (order/content)", doc.Peers)
	}
}

func TestReconciler_FailureRetriedOnTick(t *testing.T) {
	st := newWGStore(t)
	st.AddWGPeer("PKA", "", "")
	fake := newFakeSyncer()
	fake.err = context.DeadlineExceeded // every push fails
	r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
	r.interval = 30 * time.Millisecond

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go r.Run(ctx)

	r.Trigger()
	waitPush(t, fake) // the failing triggered push
	waitPush(t, fake) // the tick retry — Run must not have exited on the error
	if fake.count() < 2 {
		t.Fatalf("push count = %d, want >=2 (retry after failure)", fake.count())
	}
}

func TestReconciler_TickPushesWithoutMutations(t *testing.T) {
	st := newWGStore(t) // zero peers, zero mutations
	fake := newFakeSyncer()
	r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
	r.interval = 30 * time.Millisecond

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go r.Run(ctx)

	waitPush(t, fake) // pure tick — this IS the drift repair
	var doc struct {
		Peers []interface{} `json:"peers"`
	}
	if err := json.Unmarshal(fake.last(), &doc); err != nil {
		t.Fatalf("payload not JSON: %v", err)
	}
	if len(doc.Peers) != 0 {
		t.Errorf("empty registry must push an empty (not absent) peer list: %s", fake.last())
	}
}

func TestReconciler_RemovedPeerAbsentFromNextPayload(t *testing.T) {
	st := newWGStore(t)
	st.AddWGPeer("PKA", "", "")
	st.AddWGPeer("PKB", "", "")
	fake := newFakeSyncer()
	r := NewReconciler(st, fake, log.New(io.Discard, "", 0))

	if err := r.SyncNow(context.Background()); err != nil {
		t.Fatalf("SyncNow: %v", err)
	}
	if err := st.RemoveWGPeer("PKA"); err != nil {
		t.Fatalf("RemoveWGPeer: %v", err)
	}
	if err := r.SyncNow(context.Background()); err != nil {
		t.Fatalf("SyncNow 2: %v", err)
	}
	last := string(fake.last())
	// Assert the NEGATIVE: the removed pubkey is gone from the pushed bytes.
	if json.Valid([]byte(last)) == false {
		t.Fatalf("payload not JSON: %s", last)
	}
	if contains := jsonContainsPubkey(t, []byte(last), "PKA"); contains {
		t.Errorf("removed peer PKA still in payload: %s", last)
	}
	if contains := jsonContainsPubkey(t, []byte(last), "PKB"); !contains {
		t.Errorf("surviving peer PKB missing from payload: %s", last)
	}
}

func jsonContainsPubkey(t *testing.T, payload []byte, pk string) bool {
	t.Helper()
	var doc struct {
		Peers []struct {
			Pubkey string `json:"pubkey"`
		} `json:"peers"`
	}
	if err := json.Unmarshal(payload, &doc); err != nil {
		t.Fatalf("payload parse: %v", err)
	}
	for _, p := range doc.Peers {
		if p.Pubkey == pk {
			return true
		}
	}
	return false
}
