package main

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// setupTestDB creates a temporary kanban DB for the duration of the test.
func setupTestDB(t *testing.T) *DB {
	t.Helper()
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "test_operations.db")
	db, err := openDB(dbPath)
	if err != nil {
		t.Fatalf("openDB: %v", err)
	}
	t.Cleanup(func() { db.Close() })
	return db
}

func mustJSON(t *testing.T, v any) json.RawMessage {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	return b
}

func mustOK(t *testing.T, res ToolResult) {
	t.Helper()
	if !res.OK {
		t.Fatalf("expected ok, got error: %s", res.Error)
	}
}

func mustErr(t *testing.T, res ToolResult, contains string) {
	t.Helper()
	if res.OK {
		t.Fatalf("expected error, got ok with result: %v", res.Result)
	}
	if contains != "" && !strings.Contains(res.Error, contains) {
		t.Fatalf("error %q does not contain %q", res.Error, contains)
	}
}

// --- list_board ---

func TestExecuteTool_ListBoard_Empty(t *testing.T) {
	db := setupTestDB(t)
	res := executeTool(db, "list_board", json.RawMessage(`{}`))
	mustOK(t, res)
	board, ok := res.Result.(map[string]any)
	if !ok {
		t.Fatalf("expected map[string]any, got %T", res.Result)
	}
	cols := board["columns"].([]Column)
	cards := board["cards"].([]Card)
	if len(cols) != 0 || len(cards) != 0 {
		t.Fatalf("expected empty board, got %d cols %d cards", len(cols), len(cards))
	}
}

// --- create_column / rename_column / delete_column / reorder_columns ---

func TestExecuteTool_CreateColumn(t *testing.T) {
	db := setupTestDB(t)
	res := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Backlog"}))
	mustOK(t, res)
	col := res.Result.(*Column)
	if col.Name != "Backlog" || col.Position != 0 || col.ID == "" {
		t.Fatalf("unexpected column: %+v", col)
	}
}

func TestExecuteTool_CreateColumn_EmptyName(t *testing.T) {
	db := setupTestDB(t)
	res := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "  "}))
	mustErr(t, res, "name required")
}

func TestExecuteTool_UpdateColumn_Name(t *testing.T) {
	db := setupTestDB(t)
	created := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Old"}))
	col := created.Result.(*Column)

	res := executeTool(db, "update_column", mustJSON(t, map[string]string{"id": col.ID, "name": "New"}))
	mustOK(t, res)

	cols, _ := db.ListColumns()
	if cols[0].Name != "New" {
		t.Fatalf("rename failed: %s", cols[0].Name)
	}
}

func TestExecuteTool_UpdateColumn_LocationAndWidth(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)

	loc := "sidebar"
	width := 450
	res := executeTool(db, "update_column", mustJSON(t, map[string]any{"id": col.ID, "location": loc, "width": width}))
	mustOK(t, res)

	cols, _ := db.ListColumns()
	if cols[0].Location != "sidebar" || cols[0].Width != 450 {
		t.Fatalf("update failed: %+v", cols[0])
	}
}

func TestExecuteTool_UpdateColumn_NoFields(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)
	res := executeTool(db, "update_column", mustJSON(t, map[string]string{"id": col.ID}))
	mustErr(t, res, "at least one")
}

func TestExecuteTool_RenameColumn_AliasStillWorks(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Old"})).Result.(*Column)
	res := executeTool(db, "rename_column", mustJSON(t, map[string]string{"id": col.ID, "name": "New"}))
	mustOK(t, res)
	cols, _ := db.ListColumns()
	if cols[0].Name != "New" {
		t.Fatalf("alias rename failed")
	}
}

func TestExecuteTool_DeleteColumn(t *testing.T) {
	db := setupTestDB(t)
	created := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Tmp"}))
	col := created.Result.(*Column)

	res := executeTool(db, "delete_column", mustJSON(t, map[string]string{"id": col.ID}))
	mustOK(t, res)

	cols, _ := db.ListColumns()
	if len(cols) != 0 {
		t.Fatalf("expected 0 cols after delete, got %d", len(cols))
	}
}

func TestExecuteTool_ReorderColumns(t *testing.T) {
	db := setupTestDB(t)
	a := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "A"})).Result.(*Column)
	b := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "B"})).Result.(*Column)
	c := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "C"})).Result.(*Column)

	res := executeTool(db, "reorder_columns", mustJSON(t, map[string][]string{"ids": {c.ID, a.ID, b.ID}}))
	mustOK(t, res)

	cols, _ := db.ListColumns()
	got := []string{cols[0].Name, cols[1].Name, cols[2].Name}
	want := []string{"C", "A", "B"}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("reorder mismatch at %d: want %s got %s", i, want[i], got[i])
		}
	}
}

// --- create_card / update_card / delete_card / move_card ---

func TestExecuteTool_CreateCard_AndRequester(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Todo"})).Result.(*Column)

	res := executeTool(db, "create_card", mustJSON(t, map[string]string{
		"column_id":   col.ID,
		"requester":   "Lucas",
		"title":       "Buy milk",
		"description": "Whole milk",
	}))
	mustOK(t, res)
	card := res.Result.(*Card)
	if card.Requester != "Lucas" || card.Title != "Buy milk" || card.ColumnID != col.ID {
		t.Fatalf("unexpected card: %+v", card)
	}
}

func TestExecuteTool_CreateCard_MissingTitle(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)
	res := executeTool(db, "create_card", mustJSON(t, map[string]string{
		"column_id": col.ID,
		"title":     "",
	}))
	mustErr(t, res, "required")
}

func TestExecuteTool_UpdateCard(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)
	card := executeTool(db, "create_card", mustJSON(t, map[string]string{
		"column_id": col.ID,
		"requester": "A",
		"title":     "T1",
	})).Result.(*Card)

	newTitle := "T2"
	newReq := "B"
	color := "violet"
	res := executeTool(db, "update_card", mustJSON(t, map[string]any{
		"id":        card.ID,
		"title":     newTitle,
		"requester": newReq,
		"color":     color,
	}))
	mustOK(t, res)

	cards, _ := db.ListCardsWithTime()
	if cards[0].Title != "T2" || cards[0].Requester != "B" || cards[0].Color != "violet" {
		t.Fatalf("unexpected card after update: %+v", cards[0])
	}
}

func TestExecuteTool_DeleteCard(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)
	card := executeTool(db, "create_card", mustJSON(t, map[string]string{
		"column_id": col.ID,
		"title":     "T",
	})).Result.(*Card)

	res := executeTool(db, "delete_card", mustJSON(t, map[string]string{"id": card.ID}))
	mustOK(t, res)

	cards, _ := db.ListCardsWithTime()
	if len(cards) != 0 {
		t.Fatalf("expected 0 cards, got %d", len(cards))
	}
}

func TestExecuteTool_MoveCard_BetweenColumns_OpensHistory(t *testing.T) {
	db := setupTestDB(t)
	src := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Src"})).Result.(*Column)
	dst := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Dst"})).Result.(*Column)
	card := executeTool(db, "create_card", mustJSON(t, map[string]string{
		"column_id": src.ID,
		"title":     "Move me",
	})).Result.(*Card)

	res := executeTool(db, "move_card", mustJSON(t, map[string]any{
		"id":        card.ID,
		"column_id": dst.ID,
	}))
	mustOK(t, res)

	cards, _ := db.ListCardsWithTime()
	if cards[0].ColumnID != dst.ID {
		t.Fatalf("card not moved, still in %s", cards[0].ColumnID)
	}

	histRes := executeTool(db, "card_history", mustJSON(t, map[string]string{"id": card.ID}))
	mustOK(t, histRes)
	hist := histRes.Result.(*CardHistoryResponse).ColumnHistory
	if len(hist) != 2 {
		t.Fatalf("expected 2 history entries, got %d", len(hist))
	}
	if hist[0].ExitedAt == nil {
		t.Fatalf("first entry should be closed")
	}
	if hist[1].ExitedAt != nil {
		t.Fatalf("second entry should be open")
	}
}

func TestExecuteTool_MoveCard_RequiresIDAndColumn(t *testing.T) {
	db := setupTestDB(t)
	res := executeTool(db, "move_card", mustJSON(t, map[string]string{"id": ""}))
	mustErr(t, res, "required")
}

// --- card_history ---

func TestExecuteTool_CardHistory_Single(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)
	card := executeTool(db, "create_card", mustJSON(t, map[string]string{
		"column_id": col.ID,
		"title":     "T",
	})).Result.(*Card)

	res := executeTool(db, "card_history", mustJSON(t, map[string]string{"id": card.ID}))
	mustOK(t, res)
	hist := res.Result.(*CardHistoryResponse).ColumnHistory
	if len(hist) != 1 || hist[0].ExitedAt != nil {
		t.Fatalf("expected 1 open history entry, got %+v", hist)
	}
}

// --- find_cards ---

func TestExecuteTool_FindCards_FilterByQueryRequesterColumn(t *testing.T) {
	db := setupTestDB(t)
	col := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "X"})).Result.(*Column)
	col2 := executeTool(db, "create_column", mustJSON(t, map[string]string{"name": "Y"})).Result.(*Column)
	executeTool(db, "create_card", mustJSON(t, map[string]string{"column_id": col.ID, "requester": "Lucas", "title": "Bug fix"}))
	executeTool(db, "create_card", mustJSON(t, map[string]string{"column_id": col.ID, "requester": "Ana", "title": "Feature x"}))
	executeTool(db, "create_card", mustJSON(t, map[string]string{"column_id": col2.ID, "requester": "Lucas", "title": "Refactor"}))

	// query
	r := executeTool(db, "find_cards", mustJSON(t, map[string]string{"query": "fix"}))
	mustOK(t, r)
	cards := r.Result.([]Card)
	if len(cards) != 1 || cards[0].Title != "Bug fix" {
		t.Fatalf("query filter failed: %+v", cards)
	}

	// requester
	r = executeTool(db, "find_cards", mustJSON(t, map[string]string{"requester": "Lucas"}))
	cards = r.Result.([]Card)
	if len(cards) != 2 {
		t.Fatalf("requester filter expected 2 got %d", len(cards))
	}

	// column
	r = executeTool(db, "find_cards", mustJSON(t, map[string]string{"column_id": col2.ID}))
	cards = r.Result.([]Card)
	if len(cards) != 1 || cards[0].ColumnID != col2.ID {
		t.Fatalf("column filter failed: %+v", cards)
	}

	// combined
	r = executeTool(db, "find_cards", mustJSON(t, map[string]any{"requester": "Lucas", "column_id": col.ID}))
	cards = r.Result.([]Card)
	if len(cards) != 1 || cards[0].Title != "Bug fix" {
		t.Fatalf("combined filter failed: %+v", cards)
	}
}

// --- unknown tool ---

func TestExecuteTool_Unknown(t *testing.T) {
	db := setupTestDB(t)
	res := executeTool(db, "no_such_tool", json.RawMessage(`{}`))
	mustErr(t, res, "unknown tool")
}

// --- chat logger ---

func TestChatLogger_AppendsJSONLines(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "chat.log")
	logger := newChatLogger(path)

	logger.Log("create_column", json.RawMessage(`{"name":"A"}`), ToolResult{OK: true, Result: &Column{ID: "abc", Name: "A"}})
	logger.Log("delete_card", json.RawMessage(`{"id":"x"}`), ToolResult{OK: false, Error: "card not found"})

	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read log: %v", err)
	}
	lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
	if len(lines) != 2 {
		t.Fatalf("expected 2 log lines, got %d", len(lines))
	}
	for i, line := range lines {
		var entry ChatLogEntry
		if err := json.Unmarshal([]byte(line), &entry); err != nil {
			t.Fatalf("line %d not valid JSON: %v\n%s", i, err, line)
		}
		if entry.TS == "" {
			t.Fatalf("line %d missing TS", i)
		}
	}

	var first, second ChatLogEntry
	json.Unmarshal([]byte(lines[0]), &first)
	json.Unmarshal([]byte(lines[1]), &second)
	if first.Tool != "create_column" || !first.OK {
		t.Fatalf("unexpected first entry: %+v", first)
	}
	if second.Tool != "delete_card" || second.OK || second.Error != "card not found" {
		t.Fatalf("unexpected second entry: %+v", second)
	}
}

// --- toolMutates ---

func TestToolMutates(t *testing.T) {
	mutating := []string{"create_column", "update_column", "rename_column", "delete_column", "reorder_columns",
		"create_card", "update_card", "delete_card", "move_card"}
	readonly := []string{"list_board", "card_history", "find_cards"}

	for _, n := range mutating {
		if !toolMutates(n) {
			t.Errorf("expected %s to mutate", n)
		}
	}
	for _, n := range readonly {
		if toolMutates(n) {
			t.Errorf("expected %s to be read-only", n)
		}
	}
}
