package poke

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/pem"
	"errors"
	"io"
	"log"
	"strings"
	"sync"
	"testing"

	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
	"golang.org/x/crypto/ssh"
)

func quietLogger() *log.Logger { return log.New(io.Discard, "", 0) }

type fakePeerStore struct {
	byHost map[string]*store.WGPeer
	all    []store.WGPeer
	err    error
}

func (f *fakePeerStore) GetWGPeerForHost(hostID string) (*store.WGPeer, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.byHost[hostID], nil
}
func (f *fakePeerStore) ListWGPeers() ([]store.WGPeer, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.all, nil
}

type fakeSender struct {
	mu      sync.Mutex
	targets []string
	err     error
}

func (f *fakeSender) Poke(_ context.Context, boxWGIP string) error {
	f.mu.Lock()
	f.targets = append(f.targets, boxWGIP)
	f.mu.Unlock()
	return f.err
}
func (f *fakeSender) got() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]string(nil), f.targets...)
}

// pokeHost resolves the host's WG /32 and sends the poke there — the address is looked up, never
// caller-supplied.
func TestPokeHost_SendsToResolvedWGIP(t *testing.T) {
	st := &fakePeerStore{byHost: map[string]*store.WGPeer{
		"host-abc": {HostID: "host-abc", AssignedIP: "10.77.0.5"},
	}}
	snd := &fakeSender{}
	n := NewNotifier(st, snd, quietLogger())

	if err := n.pokeHost(context.Background(), "host-abc"); err != nil {
		t.Fatalf("pokeHost: %v", err)
	}
	got := snd.got()
	if len(got) != 1 || got[0] != "10.77.0.5" {
		t.Fatalf("sender targets = %v, want [10.77.0.5]", got)
	}
}

// A host with no WG peer is a no-op error the caller logs — never a send, never a save failure.
func TestPokeHost_NoPeerNoSend(t *testing.T) {
	snd := &fakeSender{}
	n := NewNotifier(&fakePeerStore{byHost: map[string]*store.WGPeer{}}, snd, quietLogger())
	if err := n.pokeHost(context.Background(), "unknown-host"); err == nil {
		t.Fatal("pokeHost for a peerless host returned nil, want an error")
	}
	if len(snd.got()) != 0 {
		t.Fatalf("sender was called for a peerless host: %v", snd.got())
	}
}

// A store error propagates as an error (caller logs, never fails the save) and sends nothing.
func TestPokeHost_StoreErrorNoSend(t *testing.T) {
	snd := &fakeSender{}
	n := NewNotifier(&fakePeerStore{err: errors.New("db down")}, snd, quietLogger())
	if err := n.pokeHost(context.Background(), "host-abc"); err == nil {
		t.Fatal("pokeHost on a store error returned nil")
	}
	if len(snd.got()) != 0 {
		t.Fatalf("sender called despite store error: %v", snd.got())
	}
}

// A nil *Notifier is a safe no-op (poke disabled) — the public entrypoints must not panic.
func TestNilNotifierIsNoOp(t *testing.T) {
	var n *Notifier
	n.PokeHost("h") // must not panic
	n.PokeAllHosts()
}

// Client.Poke REFUSES any target outside the WG /24 BEFORE any network I/O — the confinement the
// forced command and the WG kernel also enforce, checked here at the source.
func TestClientPoke_RefusesNonWGTarget(t *testing.T) {
	_, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("keygen: %v", err)
	}
	signer, err := ssh.NewSignerFromKey(priv)
	if err != nil {
		t.Fatalf("signer: %v", err)
	}
	pemBlock, err := ssh.MarshalPrivateKey(priv, "")
	if err != nil {
		t.Fatalf("marshal key: %v", err)
	}
	hostLine := string(ssh.MarshalAuthorizedKey(signer.PublicKey()))
	c, err := New(Config{
		Addr:        "198.51.100.1:22", // TEST-NET-2 — must never actually be dialed
		User:        "felhom-peersync",
		PrivateKey:  pem.EncodeToMemory(pemBlock),
		HostKeyLine: hostLine,
	}, quietLogger())
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	err = c.Poke(context.Background(), "8.8.8.8") // public IP — outside 10.77.0.0/24
	if err == nil || !strings.Contains(err.Error(), "refusing non-WG target") {
		t.Fatalf("Poke to a public IP: err = %v, want a pre-dial 'refusing non-WG target' refusal", err)
	}
}
