package cli

import (
	"bytes"
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"git.cloudinit.dev/coreci/orca/internal/model"
	"git.cloudinit.dev/coreci/orca/internal/store"
)

func TestWatchJobs_JSONStreaming(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "orca.db")
	db, err := store.Open(dbPath)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	defer db.Close()

	repo := store.NewJobRepo(db)
	bgCtx := context.Background()
	_ = repo.Insert(bgCtx, &model.Job{ID: "seed-job", Name: "seed", Spec: "t", Status: model.JobStatusPending})

	t.Setenv("ORCA_DB", dbPath)

	jsonOutput = true
	t.Cleanup(func() { jsonOutput = false })

	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })

	ctx, cancel := context.WithCancel(bgCtx)

	done := make(chan error, 1)
	go func() { done <- watchJobsCtx(rootCmd, ctx) }()

	// First yield is immediate (G-002); wait for it.
	time.Sleep(100 * time.Millisecond)

	_ = repo.Insert(bgCtx, &model.Job{ID: "watch-job", Name: "watch", Spec: "t", Status: model.JobStatusPending})

	// Wait for at least one ticker interval (default 1s) to capture the change.
	time.Sleep(1100 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("watchJobsCtx did not return within 2s after cancel")
	}

	output := buf.String()
	if !strings.Contains(output, `"event":"init"`) {
		t.Errorf("expected init event, got: %s", output)
	}
	if !strings.Contains(output, "watch-job") {
		t.Errorf("expected watch-job in output, got: %s", output)
	}
}

func TestWatchJobs_TableRefresh(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "orca.db")
	db, err := store.Open(dbPath)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	defer db.Close()

	repo := store.NewJobRepo(db)
	bgCtx := context.Background()
	_ = repo.Insert(bgCtx, &model.Job{ID: "seed-job", Name: "seed", Spec: "t", Status: model.JobStatusPending})

	t.Setenv("ORCA_DB", dbPath)

	jsonOutput = false
	t.Cleanup(func() { jsonOutput = false })

	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })

	ctx, cancel := context.WithCancel(bgCtx)

	done := make(chan error, 1)
	go func() { done <- watchJobsCtx(rootCmd, ctx) }()

	time.Sleep(100 * time.Millisecond)

	_ = repo.Insert(bgCtx, &model.Job{ID: "table-job", Name: "table", Spec: "t", Status: model.JobStatusPending})

	time.Sleep(1100 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("watchJobsCtx did not return within 2s after cancel")
	}

	output := buf.String()
	if !strings.Contains(output, "\033[2J\033[H") {
		t.Errorf("expected clear-screen escape in table watch output, got: %s", output)
	}
	if !strings.Contains(output, "table-job") {
		t.Errorf("expected table-job in output, got: %s", output)
	}
}

func TestWatchNodes_JSONStreaming(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "orca.db")
	db, err := store.Open(dbPath)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	defer db.Close()

	repo := store.NewNodeRepo(db)
	bgCtx := context.Background()
	_ = repo.Insert(bgCtx, &model.Node{
		ID: "seed-node", Name: "seed", Address: "addr",
		State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
	})

	t.Setenv("ORCA_DB", dbPath)

	jsonOutput = true
	t.Cleanup(func() { jsonOutput = false })

	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })

	ctx, cancel := context.WithCancel(bgCtx)

	done := make(chan error, 1)
	go func() { done <- watchNodesCtx(rootCmd, ctx) }()

	time.Sleep(100 * time.Millisecond)

	_ = repo.Insert(bgCtx, &model.Node{
		ID: "watch-node", Name: "watch", Address: "addr2",
		State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
	})

	time.Sleep(1100 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("watchNodesCtx did not return within 2s after cancel")
	}

	output := buf.String()
	initFound := false
	watchNodeFound := false
	for _, line := range strings.Split(output, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		var event map[string]any
		if err := json.Unmarshal([]byte(line), &event); err != nil {
			continue
		}
		if event["event"] == "init" {
			initFound = true
			if node, ok := event["node"].(map[string]any); ok {
				if node["id"] == "watch-node" {
					watchNodeFound = true
				}
			}
		}
	}
	if !initFound {
		t.Errorf("expected init event in JSON stream, got: %s", output)
	}
	if !watchNodeFound {
		t.Errorf("expected watch-node in JSON stream, got: %s", output)
	}
}

func TestWatchNodes_TableRefresh(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "orca.db")
	db, err := store.Open(dbPath)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	defer db.Close()

	repo := store.NewNodeRepo(db)
	bgCtx := context.Background()
	_ = repo.Insert(bgCtx, &model.Node{
		ID: "seed-node", Name: "seed", Address: "addr",
		State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
	})

	t.Setenv("ORCA_DB", dbPath)

	jsonOutput = false
	t.Cleanup(func() { jsonOutput = false })

	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })

	ctx, cancel := context.WithCancel(bgCtx)

	done := make(chan error, 1)
	go func() { done <- watchNodesCtx(rootCmd, ctx) }()

	time.Sleep(100 * time.Millisecond)

	_ = repo.Insert(bgCtx, &model.Node{
		ID: "table-node", Name: "table", Address: "addr2",
		State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
	})

	time.Sleep(1100 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("watchNodesCtx did not return within 2s after cancel")
	}

	output := buf.String()
	if !strings.Contains(output, "\033[2J\033[H") {
		t.Errorf("expected clear-screen escape in table watch output, got: %s", output)
	}
	if !strings.Contains(output, "table-node") {
		t.Errorf("expected table-node in output, got: %s", output)
	}
}

func TestRenderJobTable(t *testing.T) {
	jobs := []*model.Job{
		{ID: "j1", Name: "alpha", Status: "running", ExitCode: 0},
		{ID: "j2", Name: "beta", Status: "done", ExitCode: 0},
	}
	out := renderJobTable(jobs)
	if !strings.Contains(out, "j1") || !strings.Contains(out, "alpha") {
		t.Errorf("renderJobTable missing job 1: %s", out)
	}
	if !strings.Contains(out, "j2") || !strings.Contains(out, "beta") {
		t.Errorf("renderJobTable missing job 2: %s", out)
	}
}

func TestRenderJobTableEmpty(t *testing.T) {
	out := renderJobTable(nil)
	if !strings.Contains(out, "No jobs") {
		t.Errorf("expected empty message, got: %s", out)
	}
}

func TestRenderNodeTable(t *testing.T) {
	nodes := []*model.Node{
		{ID: "n1", Name: "alpha", Address: "localhost:8443", State: "ready"},
	}
	out := renderNodeTable(nodes)
	if !strings.Contains(out, "n1") || !strings.Contains(out, "alpha") {
		t.Errorf("renderNodeTable missing node: %s", out)
	}
}

func TestRenderNodeTableEmpty(t *testing.T) {
	out := renderNodeTable(nil)
	if !strings.Contains(out, "No nodes") {
		t.Errorf("expected empty message, got: %s", out)
	}
}
