package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

func setupApp(t *testing.T) (*App, func()) {
	t.Helper()
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "test.db")

	// Init a throwaway git repo so worktree commands work
	repoRoot := filepath.Join(dir, "repo")
	if err := os.MkdirAll(repoRoot, 0o755); err != nil {
		t.Fatalf("mkdir repo: %v", err)
	}
	mustRun(t, repoRoot, "git", "init", "-b", "master")
	mustRun(t, repoRoot, "git", "config", "user.email", "test@local")
	mustRun(t, repoRoot, "git", "config", "user.name", "test")
	// commit something
	readme := filepath.Join(repoRoot, "README.md")
	_ = os.WriteFile(readme, []byte("hi\n"), 0o644)
	mustRun(t, repoRoot, "git", "add", "-A")
	mustRun(t, repoRoot, "git", "commit", "-m", "initial")

	db, err := openDB(dbPath)
	if err != nil {
		t.Fatalf("openDB: %v", err)
	}
	app := &App{
		cfg: Config{
			Port:          0,
			DBPath:        dbPath,
			RepoRoot:      repoRoot,
			WorktreesRoot: filepath.Join(dir, "worktrees"),
		},
		db:  db,
		sse: newSSEHub(),
	}
	t.Setenv("AGENT_RUNNER_STUB", "1")
	cleanup := func() {
		db.Close()
	}
	return app, cleanup
}

func mustRun(t *testing.T, dir, name string, args ...string) {
	t.Helper()
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("%s %v: %v: %s", name, args, err, string(out))
	}
}

func TestHealth(t *testing.T) {
	app, cleanup := setupApp(t)
	defer cleanup()

	req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
	rec := httptest.NewRecorder()
	app.handleHealth(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d", rec.Code)
	}
	var body map[string]interface{}
	_ = json.Unmarshal(rec.Body.Bytes(), &body)
	if body["status"] != "ok" {
		t.Fatalf("unexpected body: %v", body)
	}
}

func TestCreateRun(t *testing.T) {
	app, cleanup := setupApp(t)
	defer cleanup()

	payload := map[string]string{
		"issue_id": "0999",
		"mode":     "agent",
		"prompt":   "test prompt",
	}
	b, _ := json.Marshal(payload)
	req := httptest.NewRequest(http.MethodPost, "/api/runs", bytes.NewReader(b))
	rec := httptest.NewRecorder()
	app.handleCreateRun(rec, req)

	if rec.Code != http.StatusCreated {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var res createRunResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
		t.Fatalf("json: %v", err)
	}
	if !strings.HasPrefix(res.RunID, "run_") {
		t.Fatalf("bad run_id: %s", res.RunID)
	}
	if !strings.HasPrefix(res.Branch, "auto/") {
		t.Fatalf("bad branch: %s", res.Branch)
	}
	// Verify row inserted
	var status string
	if err := app.db.QueryRow(`SELECT status FROM runs WHERE id = ?`, res.RunID).Scan(&status); err != nil {
		t.Fatalf("query: %v", err)
	}
	if status != "running" && status != "pending" {
		t.Fatalf("expected running/pending, got %s", status)
	}
	// Verify worktree row
	var count int
	_ = app.db.QueryRow(`SELECT COUNT(*) FROM worktrees WHERE run_id = ?`, res.RunID).Scan(&count)
	if count != 1 {
		t.Fatalf("expected 1 worktree row, got %d", count)
	}
}

func TestAbortRun(t *testing.T) {
	app, cleanup := setupApp(t)
	defer cleanup()

	// Create a run
	payload := map[string]string{"issue_id": "abort_test", "prompt": "x"}
	b, _ := json.Marshal(payload)
	rec := httptest.NewRecorder()
	app.handleCreateRun(rec, httptest.NewRequest(http.MethodPost, "/api/runs", bytes.NewReader(b)))
	if rec.Code != http.StatusCreated {
		t.Fatalf("create failed: %d %s", rec.Code, rec.Body.String())
	}
	var res createRunResponse
	_ = json.Unmarshal(rec.Body.Bytes(), &res)

	// Abort
	rec2 := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/runs/%s/abort", res.RunID), nil)
	app.handleAbortRun(rec2, req, res.RunID)

	if rec2.Code != http.StatusOK {
		t.Fatalf("abort status=%d body=%s", rec2.Code, rec2.Body.String())
	}
	var status string
	_ = app.db.QueryRow(`SELECT status FROM runs WHERE id = ?`, res.RunID).Scan(&status)
	if status != "aborted" {
		t.Fatalf("expected aborted, got %s", status)
	}
	// Worktree row marked removed
	var removed bool
	_ = app.db.QueryRow(`SELECT removed_at IS NOT NULL FROM worktrees WHERE run_id = ?`, res.RunID).Scan(&removed)
	if !removed {
		t.Fatalf("expected worktree removed_at populated")
	}
}

func TestEvidencePersist(t *testing.T) {
	app, cleanup := setupApp(t)
	defer cleanup()

	// Create run
	b, _ := json.Marshal(map[string]string{"issue_id": "ev_test"})
	rec := httptest.NewRecorder()
	app.handleCreateRun(rec, httptest.NewRequest(http.MethodPost, "/api/runs", bytes.NewReader(b)))
	var run createRunResponse
	_ = json.Unmarshal(rec.Body.Bytes(), &run)

	// Attach evidence with auto-create item
	text := "tests pass"
	evReq := evidenceRequest{
		ItemKey:     "tests_green",
		Kind:        "text",
		PayloadText: &text,
	}
	body, _ := json.Marshal(evReq)
	rec2 := httptest.NewRecorder()
	app.handleAttachEvidence(rec2, httptest.NewRequest(http.MethodPost,
		fmt.Sprintf("/api/runs/%s/evidence", run.RunID), bytes.NewReader(body)), run.RunID)
	if rec2.Code != http.StatusCreated {
		t.Fatalf("evidence status=%d body=%s", rec2.Code, rec2.Body.String())
	}

	// Verify rows
	var itemCount, evCount int
	_ = app.db.QueryRow(`SELECT COUNT(*) FROM dod_items WHERE run_id = ?`, run.RunID).Scan(&itemCount)
	_ = app.db.QueryRow(`SELECT COUNT(*) FROM dod_evidence`).Scan(&evCount)
	if itemCount != 1 || evCount != 1 {
		t.Fatalf("expected 1+1, got items=%d evidence=%d", itemCount, evCount)
	}

	// Status should have bumped to 'done'
	var status string
	_ = app.db.QueryRow(`SELECT status FROM dod_items WHERE run_id = ?`, run.RunID).Scan(&status)
	if status != "done" {
		t.Fatalf("expected done, got %s", status)
	}
}

func TestListFilter(t *testing.T) {
	app, cleanup := setupApp(t)
	defer cleanup()

	// Insert two runs with different kanban_app (unique issue_ids to avoid branch collision)
	for i, kapp := range []string{"kanban_a", "kanban_b", "kanban_a"} {
		k := kapp
		b, _ := json.Marshal(map[string]string{
			"kanban_app": k,
			"issue_id":   fmt.Sprintf("x_%s_%d", k, i),
		})
		rec := httptest.NewRecorder()
		app.handleCreateRun(rec, httptest.NewRequest(http.MethodPost, "/api/runs", bytes.NewReader(b)))
		if rec.Code != http.StatusCreated {
			t.Fatalf("setup row failed: %s", rec.Body.String())
		}
	}

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/api/runs?app=kanban_a", nil)
	app.handleListRuns(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var runs []Run
	_ = json.Unmarshal(rec.Body.Bytes(), &runs)
	if len(runs) != 2 {
		t.Fatalf("expected 2 runs for kanban_a, got %d", len(runs))
	}
	for _, r := range runs {
		if r.KanbanApp == nil || *r.KanbanApp != "kanban_a" {
			t.Fatalf("unexpected kanban_app: %v", r.KanbanApp)
		}
	}
}

// drain reads to EOF (used to discard test response bodies). Not strictly needed
// for httptest but kept for future use.
func drain(rc io.ReadCloser) { _, _ = io.Copy(io.Discard, rc); rc.Close() }
