package offsiteapply

import (
	"bytes"
	"context"
	"errors"
	"log"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)

// --- fakes ---

type fakeConsumer struct {
	pw     string
	err    error
	calls  int
	panics bool
}

func (f *fakeConsumer) Consume(_ context.Context) (string, error) {
	if f.panics {
		panic("consume must NOT be called (idempotent no-op)")
	}
	f.calls++
	return f.pw, f.err
}

type fakeScanner struct {
	fp, line string
	err      error
}

func (f *fakeScanner) Scan(_ context.Context, _ string, _ int) (string, string, error) {
	return f.fp, f.line, f.err
}

type fakeKeyGen struct{ priv, pub string }

func (f *fakeKeyGen) Generate() (string, string, error) { return f.priv, f.pub, nil }

type fakeInstaller struct {
	err          error
	calls        int
	gotPub       string
	gotPriv      string
	gotPw        string
	gotKnownHost string
}

func (f *fakeInstaller) Install(_ context.Context, _, _ string, _ int, password, privPEM, pub, knownHosts string) error {
	f.calls++
	f.gotPub, f.gotPriv, f.gotPw, f.gotKnownHost = pub, privPEM, password, knownHosts
	return f.err
}

type fakeProber struct {
	pem    string
	ok     bool
	panics bool
	calls  int
	gotKH  string
}

func (f *fakeProber) Probe(_ context.Context, _, _ string, _ int, kh string) (string, bool) {
	if f.panics {
		panic("prober must NOT be called (verify must precede the probe)")
	}
	f.calls++
	f.gotKH = kh
	return f.pem, f.ok
}

type fakeEnabler struct {
	err          error
	calls        int
	gotHost      string
	gotKnownHost string
	gotPriv      string
	gotQuotaGB   int
}

func (f *fakeEnabler) ConfigureOffbox(_ context.Context, host, _ string, _ int, _, privPEM, knownHosts string, quotaGB int) error {
	f.calls++
	f.gotHost, f.gotKnownHost, f.gotPriv, f.gotQuotaGB = host, knownHosts, privPEM, quotaGB
	return f.err
}

func newBridge(t *testing.T, o config.OffsiteConfig) (*Bridge, *fakeConsumer, *fakeInstaller, *fakeEnabler, *bytes.Buffer) {
	t.Helper()
	cfg := &config.Config{}
	cfg.Offsite = o
	cons := &fakeConsumer{pw: "the-transient-pw"}
	inst := &fakeInstaller{}
	en := &fakeEnabler{}
	var logbuf bytes.Buffer
	b := &Bridge{
		Cfg:        cfg,
		Consumer:   cons,
		Scanner:    &fakeScanner{fp: "SHA256:goodfp", line: "[h]:23 ssh-ed25519 AAAAKEY"},
		KeyGen:     &fakeKeyGen{priv: "PRIVPEM", pub: "ssh-ed25519 AAAAPUB felhom"},
		Installer:  inst,
		Enabler:    en,
		MarkerPath: filepath.Join(t.TempDir(), "offbox", "applied_marker"),
		Logger:     log.New(&logbuf, "", 0),
	}
	return b, cons, inst, en, &logbuf
}

func goodOffsite() config.OffsiteConfig {
	return config.OffsiteConfig{Enabled: true, Type: "shared", Host: "h", User: "u", Port: 23, RepoPath: "/home/felhom-repo", QuotaGB: 50, HostFingerprint: "SHA256:goodfp"}
}

// Scenario A — full apply: consume → verify-pin → install → configure offbox → marker persisted; pw not logged.
func TestBridge_AppliesEndToEnd(t *testing.T) {
	b, cons, inst, en, logbuf := newBridge(t, goodOffsite())
	if err := b.Reconcile(context.Background()); err != nil {
		t.Fatalf("reconcile: %v", err)
	}
	if cons.calls != 1 {
		t.Fatalf("consume calls = %d, want 1", cons.calls)
	}
	if inst.calls != 1 || inst.gotPw != "the-transient-pw" || inst.gotPub == "" {
		t.Fatalf("installer not called with pw+pub: %+v", inst)
	}
	if inst.gotKnownHost != "[h]:23 ssh-ed25519 AAAAKEY" {
		t.Fatalf("installer must receive the scanner-verified known_hosts to pin (no TOFU), got %q", inst.gotKnownHost)
	}
	if en.calls != 1 || en.gotHost != "h" || en.gotKnownHost != "[h]:23 ssh-ed25519 AAAAKEY" || en.gotPriv != "PRIVPEM" {
		t.Fatalf("enabler not called with the pinned known_hosts + key: %+v", en)
	}
	if en.gotQuotaGB != 50 {
		t.Fatalf("the bridge must map the descriptor's quota_gb into the target (SLICE 4), got %d", en.gotQuotaGB)
	}
	if b.readMarker() != descriptorHash(b.Cfg.Offsite) {
		t.Fatal("marker not persisted after a successful apply")
	}
	if strings.Contains(logbuf.String(), "the-transient-pw") {
		t.Fatal("the one-time password LEAKED into a log line")
	}
}

// SLICE 4 — a quota-only descriptor change re-applies (the hash includes QuotaGB), and with a working
// key it costs no password: key-auth-first re-pins + remaps the quota.
func TestBridge_QuotaChangeReappliesWithoutConsume(t *testing.T) {
	b, cons, _, en, _ := newBridge(t, goodOffsite())
	cons.panics = true
	b.Prober = &fakeProber{pem: "EXISTINGPEM", ok: true}
	// marker for the OLD quota (25) already applied; the descriptor now says 50
	old := b.Cfg.Offsite
	old.QuotaGB = 25
	_ = os.MkdirAll(filepath.Dir(b.MarkerPath), 0o700)
	_ = os.WriteFile(b.MarkerPath, []byte(descriptorHash(old)), 0o600)
	if err := b.Reconcile(context.Background()); err != nil {
		t.Fatalf("quota-change reconcile: %v", err)
	}
	if en.calls != 1 || en.gotQuotaGB != 50 {
		t.Fatalf("a quota raise must re-apply and map the NEW quota (no consume): %+v", en)
	}
}

// Key-auth-first (Scenario B) — the existing key still works: NO consume, NO install; re-verify + re-pin +
// reconfigure with the EXISTING key, marker updated.
func TestBridge_KeyAuthFirstSkipsConsume(t *testing.T) {
	b, cons, inst, en, _ := newBridge(t, goodOffsite())
	cons.panics = true // the whole point: a working key must NEVER consume the one-time password
	prober := &fakeProber{pem: "EXISTINGPEM", ok: true}
	b.Prober = prober
	if err := b.Reconcile(context.Background()); err != nil {
		t.Fatalf("key-auth-first reconcile: %v", err)
	}
	if prober.calls != 1 || prober.gotKH != "[h]:23 ssh-ed25519 AAAAKEY" {
		t.Fatalf("probe must run once with the freshly-scanned pinned known_hosts: %+v", prober)
	}
	if inst.calls != 0 {
		t.Fatal("installer must NOT run when the existing key authenticates")
	}
	if en.calls != 1 || en.gotPriv != "EXISTINGPEM" || en.gotKnownHost != "[h]:23 ssh-ed25519 AAAAKEY" {
		t.Fatalf("enabler must reconfigure with the EXISTING key + fresh pin: %+v", en)
	}
	if b.readMarker() != descriptorHash(b.Cfg.Offsite) {
		t.Fatal("marker must be updated after a key-auth-first apply")
	}
}

// Scenario C — key-auth-first must NOT weaken the fresh path: probe fails → the full
// verify→consume→install path runs unchanged (with the freshly GENERATED key).
func TestBridge_FreshGuestFallsThroughToFullPath(t *testing.T) {
	b, cons, inst, en, _ := newBridge(t, goodOffsite())
	b.Prober = &fakeProber{ok: false} // fresh guest: no key / auth refused
	if err := b.Reconcile(context.Background()); err != nil {
		t.Fatalf("fresh-guest reconcile: %v", err)
	}
	if cons.calls != 1 || inst.calls != 1 {
		t.Fatalf("fresh guest must consume+install exactly once: cons=%d inst=%d", cons.calls, inst.calls)
	}
	if en.calls != 1 || en.gotPriv != "PRIVPEM" {
		t.Fatalf("fresh guest must configure with the GENERATED key: %+v", en)
	}
	if b.readMarker() != descriptorHash(b.Cfg.Offsite) {
		t.Fatal("marker must be persisted after a full-path apply")
	}
}

// Scenario B — host-key mismatch → refuse: no consume, no install, no configure, no marker.
func TestBridge_HostKeyMismatchRefuses(t *testing.T) {
	b, cons, inst, en, _ := newBridge(t, goodOffsite())
	b.Scanner = &fakeScanner{fp: "SHA256:ATTACKER", line: "[h]:23 ssh-ed25519 EVIL"}
	b.Prober = &fakeProber{panics: true} // the probe must NEVER run when the identity check failed
	err := b.Reconcile(context.Background())
	if err == nil || !strings.Contains(err.Error(), "MISMATCH") {
		t.Fatalf("mismatch must refuse, got %v", err)
	}
	if cons.calls != 0 || inst.calls != 0 || en.calls != 0 {
		t.Fatalf("nothing may proceed on a host-key mismatch: cons=%d inst=%d en=%d", cons.calls, inst.calls, en.calls)
	}
	if b.readMarker() != "" {
		t.Fatal("no marker may be written on a mismatch")
	}
}

// Scenario C — idempotent: marker already matches → no-op, consume is NOT called.
func TestBridge_IdempotentNoReconsume(t *testing.T) {
	b, cons, inst, en, _ := newBridge(t, goodOffsite())
	cons.panics = true // Consume must not be called
	// pre-seed the marker with the current descriptor hash
	_ = os.MkdirAll(filepath.Dir(b.MarkerPath), 0o700)
	if err := os.WriteFile(b.MarkerPath, []byte(descriptorHash(b.Cfg.Offsite)), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := b.Reconcile(context.Background()); err != nil {
		t.Fatalf("idempotent reconcile must be a clean no-op, got %v", err)
	}
	if cons.calls != 0 || inst.calls != 0 || en.calls != 0 {
		t.Fatal("an already-applied descriptor must be a full no-op")
	}
}

// Scenario D — install fails → fail-safe: marker NOT persisted, offbox NOT configured, loud log.
func TestBridge_InstallFailIsFailSafe(t *testing.T) {
	b, cons, inst, en, logbuf := newBridge(t, goodOffsite())
	inst.err = errors.New("ssh-copy-id refused")
	err := b.Reconcile(context.Background())
	if err == nil {
		t.Fatal("install failure must error")
	}
	if en.calls != 0 {
		t.Fatal("offbox must NOT be configured when install fails")
	}
	if b.readMarker() != "" {
		t.Fatal("marker must NOT be persisted on a failed apply (fail-safe)")
	}
	if cons.calls != 1 {
		t.Fatal("the password was consumed (spent) before install")
	}
	if !strings.Contains(logbuf.String(), "password is spent") {
		t.Fatal("a consumed-but-failed install must log the loud 'password is spent' signal")
	}
}

// Disabled → no-op (no consume/install/configure).
func TestBridge_DisabledNoOp(t *testing.T) {
	o := goodOffsite()
	o.Enabled = false
	b, cons, inst, en, _ := newBridge(t, o)
	if err := b.Reconcile(context.Background()); err != nil {
		t.Fatal(err)
	}
	if cons.calls+inst.calls+en.calls != 0 {
		t.Fatal("disabled offsite must be a no-op")
	}
}
