package sshpush

import (
	"context"
	"errors"
	"fmt"
	"sync/atomic"
	"testing"

	"golang.org/x/crypto/ssh"

	"git.cloudinit.dev/coreci/orca/internal/emitter"
)

// --- ExecAll tests ---

func TestExecAll_AllSucceed(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	// Use one server for all peers (same addr).
	addr := srv.addr()
	peers := []string{addr, addr, addr}
	out, errs := tr.ExecAll(context.Background(), peers, "echo hello")
	for _, p := range peers {
		if e, ok := errs[p]; ok && e != nil {
			t.Errorf("peer %s: %v", p, e)
		}
		if string(out[p]) != "hello\n" {
			t.Errorf("out[%s] = %q, want hello\\n", p, out[p])
		}
	}
}

func TestExecAll_OneFails(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	// Peer "bad" returns a permanent error via a mock session.
	goodPeers := []string{addr}
	badPeer := "127.0.0.1:1" // unreachable -> transient dial error, retried, fails
	peers := append(goodPeers, badPeer)
	out, errs := tr.ExecAll(context.Background(), peers, "echo hello")
	if string(out[addr]) != "hello\n" {
		t.Errorf("good peer out = %q, want hello\\n", out[addr])
	}
	if errs[badPeer] == nil {
		t.Error("bad peer should have an error")
	}
}

func TestExecAll_WithConcurrency(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	peers := []string{addr, addr, addr, addr}
	out, errs := tr.ExecAllWithConcurrency(context.Background(), peers, "echo hello", 2)
	for _, p := range peers {
		if e := errs[p]; e != nil {
			t.Errorf("peer %s: %v", p, e)
		}
		if string(out[p]) != "hello\n" {
			t.Errorf("out[%s] = %q", p, out[p])
		}
	}
}

func TestExecAll_EmptyPeers(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	out, errs := tr.ExecAll(context.Background(), nil, "echo hello")
	if len(out) != 0 || len(errs) != 0 {
		t.Errorf("empty peers: out=%v errs=%v", out, errs)
	}
}

func TestExecAll_ContextCancelled(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	out, errs := tr.ExecAll(ctx, []string{addr, addr}, "echo hello")
	// With a cancelled context, all peers should fail.
	for _, p := range []string{addr, addr} {
		if errs[p] == nil && string(out[p]) == "" {
			// acceptable: either error or no output
		}
	}
}

// --- WriteAll tests ---

func TestWriteAll_AllSucceed(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	files := map[string][]emitter.File{
		addr: {
			{Path: "/w/a", Content: "alpha\n", Mode: "0644"},
			{Path: "/w/b", Content: "beta\n", Mode: "0644"},
		},
	}
	errs := tr.WriteAll(context.Background(), []string{addr}, files)
	for p, e := range errs {
		if e != nil {
			t.Errorf("peer %s: %v", p, e)
		}
	}
	srv.mu.Lock()
	if srv.files["/w/a"] != "alpha\n" {
		t.Errorf("file a = %q", srv.files["/w/a"])
	}
	if srv.files["/w/b"] != "beta\n" {
		t.Errorf("file b = %q", srv.files["/w/b"])
	}
	srv.mu.Unlock()
}

func TestWriteAll_OnePeerFails(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	bad := "127.0.0.1:1"
	files := map[string][]emitter.File{
		addr: {{Path: "/ok/f", Content: "ok\n", Mode: "0644"}},
		bad:  {{Path: "/fail/f", Content: "fail\n", Mode: "0644"}},
	}
	errs := tr.WriteAll(context.Background(), []string{addr, bad}, files)
	if errs[addr] != nil {
		t.Errorf("good peer should not have error, got %v", errs[addr])
	}
	if errs[bad] == nil {
		t.Error("bad peer should have error")
	}
}

func TestWriteAll_EmptyPeers(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	errs := tr.WriteAll(context.Background(), nil, nil)
	if len(errs) != 0 {
		t.Errorf("empty peers: errs=%v", errs)
	}
}

func TestWriteAll_WithConcurrency(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	files := map[string][]emitter.File{
		addr: {{Path: "/c/f", Content: "c\n", Mode: "0644"}},
	}
	errs := tr.WriteAllWithConcurrency(context.Background(), []string{addr}, files, 4)
	for _, e := range errs {
		if e != nil {
			t.Errorf("peer err: %v", e)
		}
	}
}

func TestWriteAll_Idempotent(t *testing.T) {
	srv := newFakeSSHServer(t)
	defer srv.close()
	srv.mu.Lock()
	srv.files["/i/f"] = "same\n"
	srv.mu.Unlock()
	tr := realTransport(t, srv)
	defer tr.Close()
	addr := srv.addr()
	files := map[string][]emitter.File{
		addr: {{Path: "/i/f", Content: "same\n", Mode: "0644"}},
	}
	// Capture writes to verify idempotent skip.
	var writes int64
	tr.SetSessionFactory(func(c *ssh.Client) (sshSession, error) {
		return &writeCountingSession{srv: srv, writes: &writes}, nil
	})
	// Pre-populate pool.
	client, err := tr.dial(addr)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	tr.pool.Store(addr, client)
	errs := tr.WriteAll(context.Background(), []string{addr}, files)
	if errs[addr] != nil {
		t.Errorf("WriteAll err: %v", errs[addr])
	}
	// sha256sum returns a hash that matches -> no write command.
	srv.mu.Lock()
	content := srv.files["/i/f"]
	srv.mu.Unlock()
	if content != "same\n" {
		t.Errorf("content changed to %q", content)
	}
}

// writeCountingSession counts how many write commands (mkdir + cat >)
// are issued; returns the server's file content for sha256sum.
type writeCountingSession struct {
	srv    *fakeSSHServer
	writes *int64
}

func (w *writeCountingSession) CombinedOutput(cmd string) ([]byte, error) {
	c := trim(cmd)
	if startsWith(c, "sha256sum ") {
		path := unquote(trimPrefix(c, "sha256sum "))
		path = trimSuffix(path, " 2>/dev/null")
		w.srv.mu.Lock()
		content, ok := w.srv.files[path]
		w.srv.mu.Unlock()
		if !ok {
			return []byte(""), nil
		}
		sum := sha256HexStr([]byte(content))
		return []byte(sum + "  " + path + "\n"), nil
	}
	if startsWith(c, "mkdir -p ") && contains(c, "cat >") {
		atomic.AddInt64(w.writes, 1)
		return nil, nil
	}
	return nil, nil
}
func (w *writeCountingSession) Close() error { return nil }

// Local string helpers to avoid importing strings in a way that
// conflicts with the test's existing imports.
func trim(s string) string {
	for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
		s = s[1:]
	}
	for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
		s = s[:len(s)-1]
	}
	return s
}
func startsWith(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix }
func contains(s, sub string) bool {
	return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
}
func indexOf(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}
func trimPrefix(s, prefix string) string {
	if startsWith(s, prefix) {
		return s[len(prefix):]
	}
	return s
}
func trimSuffix(s, suffix string) string {
	if len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix {
		return s[:len(s)-len(suffix)]
	}
	return s
}

func TestDefaultFanoutConcurrency(t *testing.T) {
	if DefaultFanoutConcurrency != 8 {
		t.Errorf("DefaultFanoutConcurrency = %d, want 8", DefaultFanoutConcurrency)
	}
}

func TestParseMode_Fanout(t *testing.T) {
	// parseMode is in fanout.go; sanity-check here too.
	for _, tc := range []struct{ in, want string }{
		{"0644", "644"},
		{"0755", "755"},
		{"bad", "644"},
	} {
		got := fmt.Sprintf("%o", parseMode(tc.in))
		if got != tc.want {
			t.Errorf("parseMode(%q) = %s, want %s", tc.in, got, tc.want)
		}
	}
}

var _ = errors.New
