package rotate

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/pem"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"golang.org/x/crypto/ssh"

	"incredigo/internal/discover"
	"incredigo/internal/vault"
)

// TestSSHKeyRotateRealCutover proves the full SSH cutover against an in-process
// SSH server: the new key authenticates, the old key is dropped from
// authorized_keys only after verify, and no private-key material is ever written
// to authorized_keys.
func TestSSHKeyRotateRealCutover(t *testing.T) {
	v := vault.New()
	defer v.Purge()

	authKeys := filepath.Join(t.TempDir(), "authorized_keys")

	// Seed an OLD keypair: pubkey trusted in authorized_keys, private PEM in vault.
	oldPub, oldPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("gen old key: %v", err)
	}
	oldSSHPub, err := ssh.NewPublicKey(oldPub)
	if err != nil {
		t.Fatalf("old ssh pub: %v", err)
	}
	if err := os.WriteFile(authKeys, ssh.MarshalAuthorizedKey(oldSSHPub), 0o600); err != nil {
		t.Fatalf("seed authorized_keys: %v", err)
	}
	oldBlock, err := ssh.MarshalPrivateKey(oldPriv, "old")
	if err != nil {
		t.Fatalf("marshal old priv: %v", err)
	}
	oldPEM := pem.EncodeToMemory(oldBlock)
	oldPEMCopy := append([]byte(nil), oldPEM...) // for later inequality check (Store wipes oldPEM)

	oldSigner, err := ssh.NewSignerFromKey(oldPriv)
	if err != nil {
		t.Fatalf("old signer: %v", err)
	}

	addr := startSSHServer(t, authKeys)

	cred := discover.Credential{
		Source:   "ssh",
		Kind:     discover.KindPrivateKey,
		Identity: "id_ed25519",
		Location: authKeys,
		Secret:   v.Store(oldPEM),
		Meta: map[string]string{
			"ssh_addr":        addr,
			"ssh_user":        "tester",
			"authorized_keys": authKeys,
		},
	}

	// Baseline: the old key authenticates before any rotation.
	if err := canAuth(addr, "tester", oldSigner); err != nil {
		t.Fatalf("old key should authenticate at start: %v", err)
	}

	s := &SSHKey{}
	ctx := context.Background()

	// 1. Rotate — mint new key, install its pubkey (old still trusted).
	newH, err := s.Rotate(ctx, cred, v)
	if err != nil {
		t.Fatalf("Rotate: %v", err)
	}
	nb, err := v.Open(newH)
	if err != nil {
		t.Fatalf("open new secret: %v", err)
	}
	if bytes.Equal(nb.Bytes(), oldPEMCopy) {
		t.Fatal("new private key equals old — rotation minted nothing")
	}
	if !bytes.Contains(nb.Bytes(), []byte("PRIVATE KEY")) {
		t.Fatal("new secret is not a private key PEM")
	}
	newSigner, err := ssh.ParsePrivateKey(nb.Bytes())
	if err != nil {
		t.Fatalf("parse new private key: %v", err)
	}

	// After Rotate both keys authenticate (verify-before-revoke ordering).
	if err := canAuth(addr, "tester", oldSigner); err != nil {
		t.Fatalf("old key must still work before revoke: %v", err)
	}
	if err := canAuth(addr, "tester", newSigner); err != nil {
		t.Fatalf("new key must authenticate after install: %v", err)
	}

	// 2. Verify(new) via the driver.
	if err := s.Verify(ctx, newH, v); err != nil {
		t.Fatalf("Verify(new): %v", err)
	}

	// 3. RevokeOld — drop the old pubkey.
	if err := s.RevokeOld(ctx, cred, v); err != nil {
		t.Fatalf("RevokeOld: %v", err)
	}

	// Cutover assertions: old dead, new alive.
	if err := canAuth(addr, "tester", oldSigner); err == nil {
		t.Fatal("old key must be rejected after revoke")
	}
	if err := canAuth(addr, "tester", newSigner); err != nil {
		t.Fatalf("new key must still authenticate after revoke: %v", err)
	}

	// Hard Rule #3: no private-key material in authorized_keys.
	final, err := os.ReadFile(authKeys)
	if err != nil {
		t.Fatalf("read authorized_keys: %v", err)
	}
	if bytes.Contains(final, []byte("PRIVATE KEY")) {
		t.Fatal("authorized_keys leaked private key material")
	}
	// Exactly one key should remain (the new one).
	if got := bytes.Count(bytes.TrimSpace(final), []byte("ssh-ed25519")); got != 1 {
		t.Fatalf("expected exactly 1 trusted key after cutover, found %d", got)
	}
}

// canAuth dials addr and authenticates with signer, returning nil on success.
func canAuth(addr, user string, signer ssh.Signer) error {
	cfg := &ssh.ClientConfig{
		User:            user,
		Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         5 * time.Second,
	}
	cli, err := ssh.Dial("tcp", addr, cfg)
	if err != nil {
		return err
	}
	return cli.Close()
}

// startSSHServer launches an in-process SSH server on a random localhost port that
// authorizes any public key currently present in authKeysPath (re-read per attempt,
// so it reflects rotation). It returns the listen address; the listener is closed
// at test cleanup.
func startSSHServer(t *testing.T, authKeysPath string) string {
	t.Helper()

	_, hostPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("gen host key: %v", err)
	}
	hostSigner, err := ssh.NewSignerFromKey(hostPriv)
	if err != nil {
		t.Fatalf("host signer: %v", err)
	}

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	t.Cleanup(func() { ln.Close() })

	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				return // listener closed
			}
			go serveSSH(conn, hostSigner, authKeysPath)
		}
	}()
	return ln.Addr().String()
}

func serveSSH(conn net.Conn, hostKey ssh.Signer, authKeysPath string) {
	cfg := &ssh.ServerConfig{
		PublicKeyCallback: func(_ ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
			if keyAuthorized(authKeysPath, key) {
				return &ssh.Permissions{}, nil
			}
			return nil, ssh.ErrNoAuth
		},
	}
	cfg.AddHostKey(hostKey)

	sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
	if err != nil {
		conn.Close()
		return
	}
	go ssh.DiscardRequests(reqs)
	go func() {
		for ch := range chans {
			ch.Reject(ssh.Prohibited, "no channels")
		}
	}()
	_ = sshConn
}

// keyAuthorized reports whether key matches any entry in authKeysPath.
func keyAuthorized(path string, key ssh.PublicKey) bool {
	data, err := os.ReadFile(path)
	if err != nil {
		return false
	}
	want := key.Marshal()
	for len(data) > 0 {
		pub, _, _, rest, err := ssh.ParseAuthorizedKey(data)
		if err != nil {
			break
		}
		if bytes.Equal(pub.Marshal(), want) {
			return true
		}
		data = rest
	}
	return false
}
