package provision

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"testing"
)

// recRunner records every command issued (to assert chown + pct set ran with correct args).
type recRunner struct {
	mu   sync.Mutex
	cmds [][]string
	fail string // if a command's name == fail, return an error
}

func (r *recRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
	r.mu.Lock()
	r.cmds = append(r.cmds, append([]string{name}, args...))
	r.mu.Unlock()
	if name == r.fail {
		return nil, []byte("boom"), io.ErrUnexpectedEOF
	}
	return nil, nil, nil
}

func (r *recRunner) find(name string) []string {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, c := range r.cmds {
		if c[0] == name {
			return c
		}
	}
	return nil
}

// mintMinter returns a fixed token and records the vmid it was minted for.
type mintMinter struct {
	token string
	vmids []int
}

func (m *mintMinter) Mint(vmid int) (string, error) { m.vmids = append(m.vmids, vmid); return m.token, nil }

func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }

func newInput() Input {
	return Input{
		VMID:        8200,
		Customer:    DocCustomer{ID: "cust-8200"},
		Hub:         DocHub{URL: "https://hub.felhom.eu", RetrievalPassword: "five-word-passphrase"},
		Endpoint:    "192.168.0.162:8443",
		Fingerprint: "ab12cd",
	}
}

func TestProvision_WritesChownsAndAttaches(t *testing.T) {
	dir := t.TempDir()
	runner := &recRunner{}
	minter := &mintMinter{token: "SECRET-TOKEN-XYZ"}
	bh := NewBackHalf(minter, runner, dir, testLogger())

	res, err := bh.Provision(context.Background(), newInput())
	if err != nil {
		t.Fatalf("provision: %v", err)
	}

	// token minted for the right guest
	if len(minter.vmids) != 1 || minter.vmids[0] != 8200 {
		t.Fatalf("mint vmids: %v", minter.vmids)
	}

	// bootstrap.json written 0600, contains the token + customer, valid contract
	bootPath := filepath.Join(res.HostDir, "bootstrap.json")
	info, err := os.Stat(bootPath)
	if err != nil {
		t.Fatalf("stat bootstrap: %v", err)
	}
	// Unix perms are not modeled on Windows; the 0600 is enforced on the Linux target (where the
	// agent runs). Assert only where the OS honors it.
	if runtime.GOOS != "windows" {
		if perm := info.Mode().Perm(); perm != 0o600 {
			t.Fatalf("bootstrap perms: got %o want 600", perm)
		}
	}
	raw, _ := os.ReadFile(bootPath)
	var doc Doc
	if err := json.Unmarshal(raw, &doc); err != nil {
		t.Fatalf("bootstrap not valid JSON: %v", err)
	}
	if doc.Schema != SchemaV2 || doc.Customer.ID != "cust-8200" || doc.LocalAPI.Token != "SECRET-TOKEN-XYZ" {
		t.Fatalf("bootstrap content wrong: %+v", doc)
	}
	if doc.Hub.URL != "https://hub.felhom.eu" || doc.Hub.RetrievalPassword != "five-word-passphrase" {
		t.Fatalf("hub wrong (want url + retrieval_password, no host key): %+v", doc.Hub)
	}
	if doc.LocalAPI.Endpoint != "192.168.0.162:8443" || doc.LocalAPI.Fingerprint != "ab12cd" {
		t.Fatalf("local_api wrong: %+v", doc.LocalAPI)
	}

	// chown to the mapped guest-root ran on the host dir
	chown := runner.find("chown")
	if chown == nil || chown[1] != "-R" || chown[2] != "100000:100000" || chown[3] != res.HostDir {
		t.Fatalf("chown command wrong: %v", chown)
	}

	// pct set attached the read-only bind mount at the default high slot
	pct := runner.find("pct")
	if pct == nil {
		t.Fatal("pct set not called")
	}
	joined := strings.Join(pct, " ")
	if !strings.Contains(joined, "set 8200 -mp9") || !strings.Contains(joined, res.HostDir+",mp=/etc/felhom-bootstrap,ro=1") {
		t.Fatalf("pct set command wrong: %v", pct)
	}
	if res.MountKey != "mp9" || res.GuestPath != "/etc/felhom-bootstrap" {
		t.Fatalf("result placement wrong: %+v", res)
	}
}

// The Result must never carry the token, and the token must not appear in any field returned to
// the caller (secret discipline — only the 0600 file + the store hash hold it).
func TestProvision_ResultHasNoToken(t *testing.T) {
	dir := t.TempDir()
	bh := NewBackHalf(&mintMinter{token: "SECRET-TOKEN-XYZ"}, &recRunner{}, dir, testLogger())
	res, err := bh.Provision(context.Background(), newInput())
	if err != nil {
		t.Fatal(err)
	}
	blob, _ := json.Marshal(res)
	if strings.Contains(string(blob), "SECRET-TOKEN-XYZ") {
		t.Fatalf("token leaked into the Result: %s", blob)
	}
}

func TestProvision_RejectsIncompleteInput(t *testing.T) {
	dir := t.TempDir()
	bh := NewBackHalf(&mintMinter{token: "t"}, &recRunner{}, dir, testLogger())
	bad := Input{VMID: 8200} // no endpoint/fingerprint/customer
	if _, err := bh.Provision(context.Background(), bad); err == nil {
		t.Fatal("expected an error for incomplete input")
	}
}

// A failed chown surfaces an error (and does not proceed to attach).
func TestProvision_ChownFailureStops(t *testing.T) {
	dir := t.TempDir()
	runner := &recRunner{fail: "chown"}
	bh := NewBackHalf(&mintMinter{token: "t"}, runner, dir, testLogger())
	if _, err := bh.Provision(context.Background(), newInput()); err == nil {
		t.Fatal("expected chown failure to surface")
	}
	if runner.find("pct") != nil {
		t.Fatal("pct set ran despite a chown failure")
	}
}
