package sshpush

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"

	"golang.org/x/crypto/ssh"
)

func TestSha256Hex(t *testing.T) {
	got := sha256Hex([]byte("hello"))
	want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
	if got != want {
		t.Errorf("sha256Hex = %q, want %q", got, want)
	}
}

func TestSplitDir(t *testing.T) {
	dir, base := splitDir("/etc/orca/foo.conf")
	if dir != "/etc/orca" || base != "/etc/orca" {
		t.Errorf("splitDir(/etc/orca/foo.conf) = (%q,%q), want (/etc/orca,/etc/orca)", dir, base)
	}
	dir, base = splitDir("foo.conf")
	if dir != "" || base != "." {
		t.Errorf("splitDir(foo.conf) = (%q,%q), want (\"\",.)", dir, base)
	}
}

func TestRandomToken(t *testing.T) {
	a := randomToken(8)
	b := randomToken(8)
	if a == b {
		t.Error("randomToken returned same value twice")
	}
	if len(a) != 16 { // 8 bytes hex = 16 chars
		t.Errorf("randomToken(8) len = %d, want 16", len(a))
	}
}

// --- WriteFileIdempotent tests via the fake SSH server ---

func TestWriteFileIdempotent_WritesWhenFileMissing(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	content := []byte("first content\n")
	written, err := tr.WriteFileIdempotent(context.Background(), srv.addr(), "/etc/orca/a.conf", content, 0o644)
	if err != nil {
		t.Fatalf("WriteFileIdempotent: %v", err)
	}
	if !written {
		t.Error("written=false, want true (file was missing)")
	}
	srv.mu.Lock()
	got := srv.files["/etc/orca/a.conf"]
	srv.mu.Unlock()
	if got != string(content) {
		t.Errorf("remote file = %q, want %q", got, string(content))
	}
}

func TestWriteFileIdempotent_SkipsWhenContentMatches(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	content := []byte("same content\n")
	srv.mu.Lock()
	srv.files["/etc/orca/b.conf"] = string(content)
	srv.mu.Unlock()
	tr := realTransport(t, srv)
	defer tr.Close()
	written, err := tr.WriteFileIdempotent(context.Background(), srv.addr(), "/etc/orca/b.conf", content, 0o644)
	if err != nil {
		t.Fatalf("WriteFileIdempotent: %v", err)
	}
	if written {
		t.Error("written=true, want false (content matched)")
	}
}

func TestWriteFileIdempotent_WritesWhenContentDiffers(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	srv.mu.Lock()
	srv.files["/etc/orca/c.conf"] = "old content\n"
	srv.mu.Unlock()
	tr := realTransport(t, srv)
	defer tr.Close()
	newContent := []byte("new content\n")
	written, err := tr.WriteFileIdempotent(context.Background(), srv.addr(), "/etc/orca/c.conf", newContent, 0o644)
	if err != nil {
		t.Fatalf("WriteFileIdempotent: %v", err)
	}
	if !written {
		t.Error("written=false, want true (content differed)")
	}
	srv.mu.Lock()
	got := srv.files["/etc/orca/c.conf"]
	srv.mu.Unlock()
	if got != string(newContent) {
		t.Errorf("remote file = %q, want %q", got, string(newContent))
	}
}

func TestWriteFile_DelegatesToIdempotent(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	content := []byte("delegated\n")
	if err := tr.WriteFile(context.Background(), srv.addr(), "/etc/orca/d.conf", content, 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}
	srv.mu.Lock()
	got := srv.files["/etc/orca/d.conf"]
	srv.mu.Unlock()
	if got != string(content) {
		t.Errorf("remote file = %q, want %q", got, string(content))
	}
}

// --- Pure-logic idempotency tests via mock session (no SSH server) ---

// mockHashSession returns the hash of the file matching the sha256sum
// command's path argument; for write commands (mkdir + cat >), it
// records the write. This lets us test the idempotency decision logic
// without a real SSH server.
type mockHashSession struct {
	files     map[string]string
	out       []byte
	err       error
	cmd       string
	writeHook func(cmd string)
}

func (m *mockHashSession) CombinedOutput(cmd string) ([]byte, error) {
	m.cmd = cmd
	c := strings.TrimSpace(cmd)
	if strings.HasPrefix(c, "sha256sum ") {
		rest := strings.TrimSpace(strings.TrimPrefix(c, "sha256sum "))
		rest = strings.TrimSuffix(rest, " 2>/dev/null")
		rest = strings.TrimSpace(rest)
		path := unquote(rest)
		content, ok := m.files[path]
		if !ok {
			return []byte(""), nil
		}
		sum := sha256.Sum256([]byte(content))
		return []byte(hex.EncodeToString(sum[:]) + "  " + path + "\n"), nil
	}
	if strings.HasPrefix(c, "mkdir -p ") && strings.Contains(c, "cat >") {
		if m.writeHook != nil {
			m.writeHook(c)
		}
		return nil, nil
	}
	return m.out, m.err
}
func (m *mockHashSession) Close() error { return nil }

func TestWriteFileIdempotent_MockSkip(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	files := map[string]string{"/x/f": "match"}
	var writes int
	tr.SetSessionFactory(func(c *ssh.Client) (sshSession, error) {
		return &mockHashSession{
			files:     files,
			writeHook: func(string) { writes++ },
		}, nil
	})
	client, err := tr.dial(srv.addr())
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	tr.pool.Store(srv.addr(), client)
	written, err := tr.WriteFileIdempotent(context.Background(), srv.addr(), "/x/f", []byte("match"), 0o644)
	if err != nil {
		t.Fatalf("WriteFileIdempotent: %v", err)
	}
	if written {
		t.Error("written=true, want false (hash matched)")
	}
	if writes != 0 {
		t.Errorf("writes = %d, want 0 (no write on hash match)", writes)
	}
}

func TestWriteFileIdempotent_MockWrite(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	files := map[string]string{"/x/f": "old"}
	var writes int
	tr.SetSessionFactory(func(c *ssh.Client) (sshSession, error) {
		return &mockHashSession{
			files:     files,
			writeHook: func(string) { writes++ },
		}, nil
	})
	client, err := tr.dial(srv.addr())
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	tr.pool.Store(srv.addr(), client)
	written, err := tr.WriteFileIdempotent(context.Background(), srv.addr(), "/x/f", []byte("new"), 0o644)
	if err != nil {
		t.Fatalf("WriteFileIdempotent: %v", err)
	}
	if !written {
		t.Error("written=false, want true (hash differed)")
	}
	if writes != 1 {
		t.Errorf("writes = %d, want 1", writes)
	}
}

func TestWriteFileIdempotent_MockMissingFile(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	files := map[string]string{}
	var writes int
	tr.SetSessionFactory(func(c *ssh.Client) (sshSession, error) {
		return &mockHashSession{
			files:     files,
			writeHook: func(string) { writes++ },
		}, nil
	})
	client, err := tr.dial(srv.addr())
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	tr.pool.Store(srv.addr(), client)
	written, err := tr.WriteFileIdempotent(context.Background(), srv.addr(), "/x/new", []byte("fresh"), 0o644)
	if err != nil {
		t.Fatalf("WriteFileIdempotent: %v", err)
	}
	if !written {
		t.Error("written=false, want true (file missing)")
	}
	if writes != 1 {
		t.Errorf("writes = %d, want 1", writes)
	}
}

func TestRemoteSHA256_ParsesDigest(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	srv.mu.Lock()
	srv.files["/x/h"] = "abc"
	srv.mu.Unlock()
	tr := realTransport(t, srv)
	defer tr.Close()
	got, err := tr.remoteSHA256(context.Background(), srv.addr(), "/x/h")
	if err != nil {
		t.Fatalf("remoteSHA256: %v", err)
	}
	want := fmt.Sprintf("%x", sha256.Sum256([]byte("abc")))
	if got != want {
		t.Errorf("remoteSHA256 = %q, want %q", got, want)
	}
}

func TestRemoteSHA256_MissingFile(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	got, err := t_remoteSHA256_noErr(t, tr, srv.addr(), "/missing")
	if err != nil {
		t.Fatalf("remoteSHA256: %v", err)
	}
	if got != "" {
		t.Errorf("remoteSHA256 = %q, want empty for missing file", got)
	}
}

func t_remoteSHA256_noErr(t *testing.T, tr *Transport, peer, path string) (string, error) {
	t.Helper()
	return tr.remoteSHA256(context.Background(), peer, path)
}

func TestWriteFile_ModeApplied(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	if err := tr.WriteFile(context.Background(), srv.addr(), "/m/f", []byte("mode"), 0o755); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}
	srv.mu.Lock()
	got := srv.files["/m/f"]
	srv.mu.Unlock()
	if got != "mode" {
		t.Errorf("content = %q, want mode", got)
	}
}

func TestWriteFileIdempotent_ExecError(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	tr.SetSessionFactory(func(c *ssh.Client) (sshSession, error) {
		return &mockSession{err: fmt.Errorf("%w: boom", ErrPermanent)}, nil
	})
	client, err := tr.dial(srv.addr())
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	tr.pool.Store(srv.addr(), client)
	_, err = tr.WriteFileIdempotent(context.Background(), srv.addr(), "/x/f", []byte("z"), 0o644)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestParseMode(t *testing.T) {
	for _, tc := range []struct {
		in   string
		want os.FileMode
	}{
		{"0644", 0o644},
		{"0755", 0o755},
		{"0600", 0o600},
		{"bad", 0o644},
		{"", 0o644},
		{"0", 0o644},
	} {
		got := parseMode(tc.in)
		if got != tc.want {
			t.Errorf("parseMode(%q) = %o, want %o", tc.in, got, tc.want)
		}
	}
}

var _ = errors.New
