package e2e_test

import (
	"slices"
	"strings"
	"testing"
)

func TestCRUD(t *testing.T) {
	env := newTestEnv(t, "testuser")

	var issueID, issue2ID, noteID string

	// ── Add ──────────────────────────────────────────────────────────────────

	t.Run("Add_IssueDefaults", func(t *testing.T) {
		out := env.mustAx("add", "My first issue", "--json")
		n := env.parseNode(out)
		issueID = n.ID

		if n.Title != "My first issue" {
			t.Errorf("title: want %q, got %q", "My first issue", n.Title)
		}
		if n.Property("type") != "issue" {
			t.Errorf("type: want issue, got %q", n.Property("type"))
		}
		if n.Property("status") != "open" {
			t.Errorf("status: want open, got %q", n.Property("status"))
		}
		if n.Property("prio") != "" {
			t.Errorf("prio: want empty, got %q", n.Property("prio"))
		}
		if len(n.Relations["created"]) == 0 {
			t.Error("expected created relation to be set")
		}
		if n.CreatedAt == "" || n.UpdatedAt == "" {
			t.Error("expected timestamps to be set")
		}
	})

	t.Run("Add_WithAllFlags", func(t *testing.T) {
		out := env.mustAx("add", "Full issue",
			"--prio", "high",
			"--status", "open",
			"--namespace", "myproject",
			"--assignee", "alice",
			"--tag", "backend",
			"--tag", "urgent",
			"--content", "some body",
			"--json",
		)
		n := env.parseNode(out)
		issue2ID = n.ID

		if n.Property("type") != "issue" {
			t.Errorf("type: want issue, got %q", n.Property("type"))
		}
		if n.Property("prio") != "high" {
			t.Errorf("prio: want high, got %q", n.Property("prio"))
		}
		if n.Property("status") != "open" {
			t.Errorf("status: want open, got %q", n.Property("status"))
		}
		if !n.HasTag("backend") {
			t.Error("expected tag 'backend'")
		}
		if !n.HasTag("urgent") {
			t.Error("expected tag 'urgent'")
		}
		if n.Content != "some body" {
			t.Errorf("content: want %q, got %q", "some body", n.Content)
		}
		if len(n.Relations["assignee"]) == 0 {
			t.Error("expected assignee relation")
		}
	})

	t.Run("Add_NoteType_NoDefaultStatus", func(t *testing.T) {
		out := env.mustAx("add", "A note", "--type", "note", "--json")
		n := env.parseNode(out)
		noteID = n.ID

		if n.Property("type") != "note" {
			t.Errorf("type: want note, got %q", n.Property("type"))
		}
		if n.Property("status") != "" {
			t.Errorf("note should have no default status, got %q", n.Property("status"))
		}
	})

	t.Run("Add_CustomRelation", func(t *testing.T) {
		if issueID == "" || issue2ID == "" {
			t.Skip("prerequisite nodes missing")
		}
		out := env.mustAx("update", issue2ID, "--rel", "blocks:"+issueID, "--json")
		n := env.parseNode(out)
		if !n.HasRelation("blocks", issueID) {
			t.Errorf("expected blocks relation to %s, got relations: %v", issueID, n.Relations)
		}
	})

	// ── Show ─────────────────────────────────────────────────────────────────

	t.Run("Show_Fields", func(t *testing.T) {
		if issue2ID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("show", issue2ID, "--json")
		n := env.parseNode(out)

		if n.ID != issue2ID {
			t.Errorf("id: want %q, got %q", issue2ID, n.ID)
		}
		if n.Title != "Full issue" {
			t.Errorf("title: want %q, got %q", "Full issue", n.Title)
		}
		if n.Content != "some body" {
			t.Errorf("content: want %q, got %q", "some body", n.Content)
		}
	})

	t.Run("Show_NotFound_Fails", func(t *testing.T) {
		_, err := env.ax("show", "xxxxx")
		if err == nil {
			t.Fatal("expected error for unknown ID, got none")
		}
	})

	// ── List ─────────────────────────────────────────────────────────────────

	t.Run("List_All", func(t *testing.T) {
		out := env.mustAx("list", "--json")
		nodes := env.parseNodes(out)
		if len(nodes) < 3 {
			t.Errorf("expected at least 3 nodes, got %d", len(nodes))
		}
	})

	t.Run("List_ByStatus", func(t *testing.T) {
		out := env.mustAx("list", "--status", "open", "--json")
		nodes := env.parseNodes(out)
		for _, n := range nodes {
			if n.Property("status") != "open" {
				t.Errorf("node %s has status %q, expected only open", n.ID, n.Property("status"))
			}
		}
		if issueID != "" {
			if _, ok := env.findInList(nodes, issueID); !ok {
				t.Errorf("expected issueID %s in open list", issueID)
			}
		}
	})

	t.Run("List_ByPrio", func(t *testing.T) {
		if issue2ID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("list", "--prio", "high", "--json")
		nodes := env.parseNodes(out)
		for _, n := range nodes {
			if n.Property("prio") != "high" {
				t.Errorf("node %s has prio %q, expected only high", n.ID, n.Property("prio"))
			}
		}
		if _, ok := env.findInList(nodes, issue2ID); !ok {
			t.Errorf("expected issue2ID %s in high-prio list", issue2ID)
		}
		if issueID != "" {
			if _, ok := env.findInList(nodes, issueID); ok {
				t.Errorf("issueID %s has no prio, should not appear in high-prio list", issueID)
			}
		}
	})

	t.Run("List_ByType", func(t *testing.T) {
		if noteID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("list", "--type", "note", "--json")
		nodes := env.parseNodes(out)
		for _, n := range nodes {
			if n.Property("type") != "note" {
				t.Errorf("node %s has type %q, expected only notes", n.ID, n.Property("type"))
			}
		}
		if _, ok := env.findInList(nodes, noteID); !ok {
			t.Errorf("expected noteID %s in note list", noteID)
		}
		if issueID != "" {
			if _, ok := env.findInList(nodes, issueID); ok {
				t.Errorf("issueID %s should not appear in note list", issueID)
			}
		}
	})

	t.Run("List_ByNamespace", func(t *testing.T) {
		if issue2ID == "" || issueID == "" {
			t.Skip("prerequisite nodes missing")
		}
		out := env.mustAx("list", "--namespace", "myproject", "--json")
		nodes := env.parseNodes(out)
		if _, ok := env.findInList(nodes, issue2ID); !ok {
			t.Errorf("issue2ID %s should be in namespace 'myproject'", issue2ID)
		}
		if _, ok := env.findInList(nodes, issueID); ok {
			t.Errorf("issueID %s should NOT be in namespace 'myproject'", issueID)
		}
	})

	t.Run("List_ByAssignee", func(t *testing.T) {
		if issue2ID == "" || issueID == "" {
			t.Skip("prerequisite nodes missing")
		}
		out := env.mustAx("list", "--assignee", "alice", "--json")
		nodes := env.parseNodes(out)
		if _, ok := env.findInList(nodes, issue2ID); !ok {
			t.Errorf("issue2ID %s should appear in alice's assignments", issue2ID)
		}
		if _, ok := env.findInList(nodes, issueID); ok {
			t.Errorf("issueID %s is not assigned to alice", issueID)
		}
	})

	t.Run("List_ByTag", func(t *testing.T) {
		if issue2ID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("list", "--tag", "backend", "--json")
		nodes := env.parseNodes(out)
		for _, n := range nodes {
			if !n.HasTag("backend") {
				t.Errorf("node %s lacks 'backend' tag", n.ID)
			}
		}
		if _, ok := env.findInList(nodes, issue2ID); !ok {
			t.Errorf("issue2ID %s (tagged backend) should be in result", issue2ID)
		}
	})

	t.Run("List_MultipleTagFilters", func(t *testing.T) {
		if issue2ID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("list", "--tag", "backend", "--tag", "urgent", "--json")
		nodes := env.parseNodes(out)
		for _, n := range nodes {
			if !n.HasTag("backend") || !n.HasTag("urgent") {
				t.Errorf("node %s does not have both tags", n.ID)
			}
		}
		if _, ok := env.findInList(nodes, issue2ID); !ok {
			t.Errorf("issue2ID %s should match both tags", issue2ID)
		}
	})

	t.Run("List_NoMatch_ReturnsEmpty", func(t *testing.T) {
		out := env.mustAx("list", "--tag", "nonexistent-tag-xyz", "--json")
		nodes := env.parseNodes(out)
		if len(nodes) != 0 {
			t.Errorf("expected empty result, got %d nodes", len(nodes))
		}
	})

	// ── Update ───────────────────────────────────────────────────────────────

	t.Run("Update_Title", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", issueID, "--title", "Updated title", "--json")
		n := env.parseNode(out)
		if n.Title != "Updated title" {
			t.Errorf("title: want %q, got %q", "Updated title", n.Title)
		}
	})

	t.Run("Update_Content", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", issueID, "--content", "New content body", "--json")
		n := env.parseNode(out)
		if n.Content != "New content body" {
			t.Errorf("content: want %q, got %q", "New content body", n.Content)
		}
	})

	t.Run("Update_Priority", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", issueID, "--prio", "medium", "--json")
		n := env.parseNode(out)
		if n.Property("prio") != "medium" {
			t.Errorf("prio: want medium, got %q", n.Property("prio"))
		}
		var prioTags []string
		for _, tag := range n.Tags {
			if strings.HasPrefix(tag, "_prio::") {
				prioTags = append(prioTags, tag)
			}
		}
		if len(prioTags) != 1 {
			t.Errorf("expected exactly 1 _prio tag, got %v", prioTags)
		}
	})

	t.Run("Update_AddAndRemoveTag", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		env.mustAx("update", issueID, "--tag", "mytag")
		out := env.mustAx("show", issueID, "--json")
		if !env.parseNode(out).HasTag("mytag") {
			t.Error("expected tag 'mytag' after add")
		}

		env.mustAx("update", issueID, "--tag-remove", "mytag")
		out = env.mustAx("show", issueID, "--json")
		if env.parseNode(out).HasTag("mytag") {
			t.Error("expected tag 'mytag' to be gone after remove")
		}
	})

	t.Run("Update_Assignee", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", issueID, "--assignee", "bob", "--json")
		n := env.parseNode(out)
		if len(n.Relations["assignee"]) == 0 {
			t.Error("expected assignee relation after --assignee bob")
		}
		listOut := env.mustAx("list", "--assignee", "bob", "--json")
		listed := env.parseNodes(listOut)
		if _, ok := env.findInList(listed, issueID); !ok {
			t.Errorf("issueID %s not found in --assignee bob list", issueID)
		}
	})

	t.Run("Update_Assignee_Replaces_Previous", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", issueID, "--assignee", "carol", "--json")
		n := env.parseNode(out)
		if len(n.Relations["assignee"]) != 1 {
			t.Errorf("expected exactly 1 assignee, got %v", n.Relations["assignee"])
		}
		listOut := env.mustAx("list", "--assignee", "bob", "--json")
		listed := env.parseNodes(listOut)
		if _, ok := env.findInList(listed, issueID); ok {
			t.Errorf("issueID %s still appears under bob after reassigning to carol", issueID)
		}
	})

	t.Run("Update_AddAndRemoveRelation", func(t *testing.T) {
		if issueID == "" || noteID == "" {
			t.Skip("prerequisite nodes missing")
		}
		env.mustAx("update", issueID, "--rel", "related:"+noteID)
		out := env.mustAx("show", issueID, "--json")
		n := env.parseNode(out)
		if !n.HasRelation("related", noteID) {
			t.Errorf("expected related relation to %s", noteID)
		}

		env.mustAx("update", issueID, "--rel-remove", "related:"+noteID)
		out = env.mustAx("show", issueID, "--json")
		n = env.parseNode(out)
		if n.HasRelation("related", noteID) {
			t.Errorf("related relation to %s should be gone after rel-remove", noteID)
		}
	})

	t.Run("Update_Namespace", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		env.mustAx("update", issueID, "--namespace", "otherproject")
		out := env.mustAx("list", "--namespace", "otherproject", "--json")
		nodes := env.parseNodes(out)
		if _, ok := env.findInList(nodes, issueID); !ok {
			t.Errorf("issueID %s should be in namespace 'otherproject' after update", issueID)
		}
		prevOut := env.mustAx("list", "--namespace", "testuser", "--json")
		prev := env.parseNodes(prevOut)
		if _, ok := env.findInList(prev, issueID); ok {
			t.Errorf("issueID %s should NOT be in old namespace after namespace update", issueID)
		}
	})

	// ── Blocking ─────────────────────────────────────────────────────────────

	t.Run("Blocking_CannotCloseBlockedIssue", func(t *testing.T) {
		if issueID == "" || issue2ID == "" {
			t.Skip("prerequisite nodes missing")
		}
		// issue2ID blocks issueID (set in Add_CustomRelation); issue2ID is still open.
		out, err := env.ax("update", issueID, "--status", "done")
		if err == nil {
			t.Fatal("expected error closing blocked issue, got none")
		}
		if !strings.Contains(out, "cannot close") {
			t.Errorf("expected 'cannot close' in output, got: %s", out)
		}
	})

	t.Run("Blocking_CloseBlockerThenClose", func(t *testing.T) {
		if issueID == "" || issue2ID == "" {
			t.Skip("prerequisite nodes missing")
		}
		env.mustAx("update", issue2ID, "--status", "done")
		out := env.mustAx("update", issueID, "--status", "done", "--json")
		n := env.parseNode(out)
		if n.Property("status") != "done" {
			t.Errorf("status: want done, got %q", n.Property("status"))
		}
	})

	t.Run("Blocking_ReopenAndVerify", func(t *testing.T) {
		if issueID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", issueID, "--status", "open", "--json")
		n := env.parseNode(out)
		if n.Property("status") != "open" {
			t.Errorf("status: want open after reopen, got %q", n.Property("status"))
		}
	})

	// ── Mentions ─────────────────────────────────────────────────────────────

	var mentionNodeID string

	t.Run("Mentions_AutoCreateUser", func(t *testing.T) {
		out := env.mustAx("add", "Review this @diana", "--json")
		n := env.parseNode(out)
		mentionNodeID = n.ID

		if len(n.Relations["mentions"]) == 0 {
			t.Fatal("expected mentions relation to be created for @diana")
		}
		dianaID := n.Relations["mentions"][0]
		userOut := env.mustAx("show", dianaID, "--json")
		user := env.parseNode(userOut)
		if user.Title != "diana" {
			t.Errorf("mention target title: want diana, got %q", user.Title)
		}
		if user.Property("type") != "user" {
			t.Errorf("mention target type: want user, got %q", user.Property("type"))
		}
	})

	t.Run("Mentions_InboxFilter", func(t *testing.T) {
		if mentionNodeID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("list", "--mention", "diana", "--json")
		nodes := env.parseNodes(out)
		if _, ok := env.findInList(nodes, mentionNodeID); !ok {
			t.Errorf("mentionNodeID %s should appear in diana's inbox", mentionNodeID)
		}
	})

	t.Run("Mentions_SyncOnTitleUpdate", func(t *testing.T) {
		if mentionNodeID == "" {
			t.Skip("prerequisite node missing")
		}
		out := env.mustAx("update", mentionNodeID, "--title", "Review this @eve", "--json")
		n := env.parseNode(out)

		if len(n.Relations["mentions"]) != 1 {
			t.Errorf("expected exactly 1 mention after update, got %v", n.Relations["mentions"])
		}
		eveID := n.Relations["mentions"][0]
		userOut := env.mustAx("show", eveID, "--json")
		if env.parseNode(userOut).Title != "eve" {
			t.Errorf("mention target after update should be eve")
		}
		dianaList := env.mustAx("list", "--mention", "diana", "--json")
		dianaNodes := env.parseNodes(dianaList)
		if _, ok := env.findInList(dianaNodes, mentionNodeID); ok {
			t.Errorf("mentionNodeID %s should no longer be in diana's inbox", mentionNodeID)
		}
		eveList := env.mustAx("list", "--mention", "eve", "--json")
		eveNodes := env.parseNodes(eveList)
		if _, ok := env.findInList(eveNodes, mentionNodeID); !ok {
			t.Errorf("mentionNodeID %s should now be in eve's inbox", mentionNodeID)
		}
	})

	t.Run("Mentions_ContentMention", func(t *testing.T) {
		out := env.mustAx("add", "No mention in title",
			"--content", "But @frank is mentioned here", "--json")
		n := env.parseNode(out)
		if len(n.Relations["mentions"]) == 0 {
			t.Fatal("expected mention from content field")
		}
		frankID := n.Relations["mentions"][0]
		frankOut := env.mustAx("show", frankID, "--json")
		if env.parseNode(frankOut).Title != "frank" {
			t.Error("content mention should create user frank")
		}
	})

	// ── Users ─────────────────────────────────────────────────────────────────

	t.Run("Users_ListedAsNodes", func(t *testing.T) {
		out := env.mustAx("list", "--type", "user", "--json")
		users := env.parseNodes(out)
		titles := make([]string, len(users))
		for i, u := range users {
			titles[i] = u.Title
		}
		for _, name := range []string{"testuser", "alice", "diana", "eve", "frank"} {
			if !slices.Contains(titles, name) {
				t.Errorf("expected user %q to exist, got users: %v", name, titles)
			}
		}
	})

	// ── Delete ───────────────────────────────────────────────────────────────

	t.Run("Delete_Force", func(t *testing.T) {
		out := env.mustAx("add", "Delete me", "--json")
		id := env.parseNode(out).ID

		env.mustAx("del", id, "--force")

		_, err := env.ax("show", id)
		if err == nil {
			t.Errorf("show after delete should fail, node %s still exists", id)
		}
	})

	t.Run("Delete_NotFound_Fails", func(t *testing.T) {
		_, err := env.ax("del", "xxxxx", "--force")
		if err == nil {
			t.Fatal("expected error deleting non-existent node, got none")
		}
	})
}
