/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-empty-function */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, within, waitFor, act } from "@testing-library/react";
import { KanbanBoard } from "./KanbanBoard";
import type { Task } from "@mosaic/shared";
import { TaskStatus, TaskPriority } from "@mosaic/shared";
import type { ToastContextValue } from "@mosaic/ui";

// Mock fetch globally
global.fetch = vi.fn();

// Mock useToast hook from @mosaic/ui
const mockShowToast = vi.fn();
vi.mock("@mosaic/ui", () => ({
  useToast: (): ToastContextValue => ({
    showToast: mockShowToast,
    removeToast: vi.fn(),
  }),
}));

// Mock the api client's apiPatch function
const mockApiPatch = vi.fn<(endpoint: string, data: unknown) => Promise<unknown>>();
vi.mock("@/lib/api/client", () => ({
  apiPatch: (endpoint: string, data: unknown): Promise<unknown> => mockApiPatch(endpoint, data),
}));

// Store drag event handlers for testing
type DragEventHandler = (event: {
  active: { id: string };
  over: { id: string } | null;
}) => Promise<void> | void;
let capturedOnDragEnd: DragEventHandler | null = null;

// Mock @dnd-kit modules
vi.mock("@dnd-kit/core", async () => {
  const actual = await vi.importActual("@dnd-kit/core");
  return {
    ...actual,
    DndContext: ({
      children,
      onDragEnd,
    }: {
      children: React.ReactNode;
      onDragEnd?: DragEventHandler;
    }): React.JSX.Element => {
      // Capture the event handler for testing
      capturedOnDragEnd = onDragEnd ?? null;
      return <div data-testid="dnd-context">{children}</div>;
    },
  };
});

vi.mock("@dnd-kit/sortable", () => ({
  SortableContext: ({ children }: { children: React.ReactNode }): React.JSX.Element => (
    <div data-testid="sortable-context">{children}</div>
  ),
  verticalListSortingStrategy: {},
  useSortable: (): object => ({
    attributes: {},
    listeners: {},
    setNodeRef: (): void => {},
    transform: null,
    transition: null,
  }),
}));

const mockTasks: Task[] = [
  {
    id: "task-1",
    title: "Design homepage",
    description: "Create wireframes for the new homepage",
    status: TaskStatus.NOT_STARTED,
    priority: TaskPriority.HIGH,
    dueDate: new Date("2026-02-01"),
    assigneeId: "user-1",
    creatorId: "user-1",
    workspaceId: "workspace-1",
    projectId: null,
    parentId: null,
    sortOrder: 0,
    metadata: {},
    completedAt: null,
    createdAt: new Date("2026-01-28"),
    updatedAt: new Date("2026-01-28"),
  },
  {
    id: "task-2",
    title: "Implement authentication",
    description: "Add OAuth support",
    status: TaskStatus.IN_PROGRESS,
    priority: TaskPriority.HIGH,
    dueDate: new Date("2026-01-30"),
    assigneeId: "user-2",
    creatorId: "user-1",
    workspaceId: "workspace-1",
    projectId: null,
    parentId: null,
    sortOrder: 1,
    metadata: {},
    completedAt: null,
    createdAt: new Date("2026-01-28"),
    updatedAt: new Date("2026-01-28"),
  },
  {
    id: "task-3",
    title: "Write unit tests",
    description: "Achieve 85% coverage",
    status: TaskStatus.PAUSED,
    priority: TaskPriority.MEDIUM,
    dueDate: new Date("2026-02-05"),
    assigneeId: "user-3",
    creatorId: "user-1",
    workspaceId: "workspace-1",
    projectId: null,
    parentId: null,
    sortOrder: 2,
    metadata: {},
    completedAt: null,
    createdAt: new Date("2026-01-28"),
    updatedAt: new Date("2026-01-28"),
  },
  {
    id: "task-4",
    title: "Deploy to production",
    description: "Set up CI/CD pipeline",
    status: TaskStatus.COMPLETED,
    priority: TaskPriority.LOW,
    dueDate: new Date("2026-01-25"),
    assigneeId: "user-1",
    creatorId: "user-1",
    workspaceId: "workspace-1",
    projectId: null,
    parentId: null,
    sortOrder: 3,
    metadata: {},
    completedAt: new Date("2026-01-25"),
    createdAt: new Date("2026-01-20"),
    updatedAt: new Date("2026-01-25"),
  },
];

describe("KanbanBoard", (): void => {
  const mockOnStatusChange = vi.fn();

  beforeEach((): void => {
    vi.clearAllMocks();
    mockShowToast.mockClear();
    mockApiPatch.mockClear();
    // Default: apiPatch succeeds
    mockApiPatch.mockResolvedValue({});
    // Also set up fetch mock for other tests that may use it
    (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
      ok: true,
      json: (): Promise<object> => Promise.resolve({}),
    } as Response);
  });

  describe("Rendering", (): void => {
    it("should render all four status columns with spec names", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Spec requires: todo, in_progress, review, done
      expect(screen.getByText("To Do")).toBeInTheDocument();
      expect(screen.getByText("In Progress")).toBeInTheDocument();
      expect(screen.getByText("Review")).toBeInTheDocument();
      expect(screen.getByText("Done")).toBeInTheDocument();
    });

    it("should organize tasks by status into correct columns", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      const todoColumn = screen.getByTestId("column-NOT_STARTED");
      const inProgressColumn = screen.getByTestId("column-IN_PROGRESS");
      const reviewColumn = screen.getByTestId("column-PAUSED");
      const doneColumn = screen.getByTestId("column-COMPLETED");

      expect(within(todoColumn).getByText("Design homepage")).toBeInTheDocument();
      expect(within(inProgressColumn).getByText("Implement authentication")).toBeInTheDocument();
      expect(within(reviewColumn).getByText("Write unit tests")).toBeInTheDocument();
      expect(within(doneColumn).getByText("Deploy to production")).toBeInTheDocument();
    });

    it("should render empty state when no tasks provided", (): void => {
      render(<KanbanBoard tasks={[]} onStatusChange={mockOnStatusChange} />);

      expect(screen.getByText("To Do")).toBeInTheDocument();
      expect(screen.getByText("In Progress")).toBeInTheDocument();
      expect(screen.getByText("Review")).toBeInTheDocument();
      expect(screen.getByText("Done")).toBeInTheDocument();
    });

    it("should handle undefined tasks array gracefully", (): void => {
      // @ts-expect-error Testing error case
      render(<KanbanBoard tasks={undefined} onStatusChange={mockOnStatusChange} />);

      expect(screen.getByText("To Do")).toBeInTheDocument();
    });
  });

  describe("Task Cards", (): void => {
    it("should display task title on each card", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      expect(screen.getByText("Design homepage")).toBeInTheDocument();
      expect(screen.getByText("Implement authentication")).toBeInTheDocument();
      expect(screen.getByText("Write unit tests")).toBeInTheDocument();
      expect(screen.getByText("Deploy to production")).toBeInTheDocument();
    });

    it("should display task priority badge", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      const highPriorityElements = screen.getAllByText("High");
      const mediumPriorityElements = screen.getAllByText("Medium");
      const lowPriorityElements = screen.getAllByText("Low");

      expect(highPriorityElements.length).toBeGreaterThan(0);
      expect(mediumPriorityElements.length).toBeGreaterThan(0);
      expect(lowPriorityElements.length).toBeGreaterThan(0);
    });

    it("should display due date when available", (): void => {
      // Use tasks with future due dates to ensure they render
      const tasksWithDates: Task[] = [
        {
          ...mockTasks[0]!,
          dueDate: new Date("2026-03-15"),
        },
        {
          ...mockTasks[1]!,
          dueDate: new Date("2026-04-20"),
        },
      ];

      render(<KanbanBoard tasks={tasksWithDates} onStatusChange={mockOnStatusChange} />);

      // Check that calendar icons are present for tasks with due dates
      const calendarIcons = screen.getAllByLabelText("Due date");
      expect(calendarIcons.length).toBeGreaterThanOrEqual(2);
    });

    it("should display assignee avatar when assignee data is provided", (): void => {
      const tasksWithAssignee: Task[] = [
        {
          ...mockTasks[0]!,
          // Task type uses assigneeId, not assignee object
          assigneeId: "user-john",
        },
      ];

      render(<KanbanBoard tasks={tasksWithAssignee} onStatusChange={mockOnStatusChange} />);

      // Check that the component renders the board successfully
      expect(screen.getByTestId("kanban-grid")).toBeInTheDocument();
      // Check that the task title is rendered
      expect(screen.getByText("Design homepage")).toBeInTheDocument();
    });
  });

  describe("Drag and Drop", (): void => {
    it("should initialize DndContext for drag-and-drop", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      expect(screen.getByTestId("dnd-context")).toBeInTheDocument();
    });

    it("should have droppable columns", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      const columns = screen.getAllByTestId(/^column-/);
      expect(columns.length).toBe(4);
    });
  });

  describe("Status Update API Call", (): void => {
    it("should call PATCH /api/tasks/:id when status changes", (): void => {
      const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
      fetchMock.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ status: TaskStatus.IN_PROGRESS }),
      } as unknown as Response);

      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Simulate drag end by calling the component's internal method
      // In a real test, we'd simulate actual drag-and-drop events
      // For now, we'll test the fetch call directly

      // This is a simplified test - full E2E would use Playwright
      expect(screen.getByTestId("dnd-context")).toBeInTheDocument();
    });

    it("should handle API errors gracefully", (): void => {
      const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

      fetchMock.mockResolvedValueOnce({
        ok: false,
        statusText: "Internal Server Error",
      } as Response);

      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Component should still render even if API fails
      expect(screen.getByText("To Do")).toBeInTheDocument();

      consoleErrorSpy.mockRestore();
    });
  });

  describe("Optimistic Updates and Rollback", (): void => {
    it("should apply optimistic update immediately on drag", async (): Promise<void> => {
      // apiPatch is already mocked to succeed in beforeEach
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Verify initial state - task-1 is in NOT_STARTED column
      const todoColumn = screen.getByTestId("column-NOT_STARTED");
      expect(within(todoColumn).getByText("Design homepage")).toBeInTheDocument();

      // Trigger drag end event to move task-1 to IN_PROGRESS and wait for completion
      await act(async () => {
        if (capturedOnDragEnd) {
          const result = capturedOnDragEnd({
            active: { id: "task-1" },
            over: { id: TaskStatus.IN_PROGRESS },
          });
          if (result instanceof Promise) {
            await result;
          }
        }
      });

      // After the drag completes, task should be in the new column (optimistic update persisted)
      const inProgressColumn = screen.getByTestId("column-IN_PROGRESS");
      expect(within(inProgressColumn).getByText("Design homepage")).toBeInTheDocument();

      // Verify the task is NOT in the original column anymore
      const todoColumnAfter = screen.getByTestId("column-NOT_STARTED");
      expect(within(todoColumnAfter).queryByText("Design homepage")).not.toBeInTheDocument();
    });

    it("should persist update when API call succeeds", async (): Promise<void> => {
      // apiPatch is already mocked to succeed in beforeEach
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Trigger drag end event
      await act(async () => {
        if (capturedOnDragEnd) {
          const result = capturedOnDragEnd({
            active: { id: "task-1" },
            over: { id: TaskStatus.IN_PROGRESS },
          });
          if (result instanceof Promise) {
            await result;
          }
        }
      });

      // Wait for API call to complete
      await waitFor(() => {
        expect(mockApiPatch).toHaveBeenCalledWith("/api/tasks/task-1", {
          status: TaskStatus.IN_PROGRESS,
        });
      });

      // Verify task is in the new column after API success
      const inProgressColumn = screen.getByTestId("column-IN_PROGRESS");
      expect(within(inProgressColumn).getByText("Design homepage")).toBeInTheDocument();

      // Verify callback was called
      expect(mockOnStatusChange).toHaveBeenCalledWith("task-1", TaskStatus.IN_PROGRESS);
    });

    it("should rollback to original position when API call fails", async (): Promise<void> => {
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

      // Mock API failure
      mockApiPatch.mockRejectedValueOnce(new Error("Network error"));

      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Verify initial state - task-1 is in NOT_STARTED column
      const todoColumnBefore = screen.getByTestId("column-NOT_STARTED");
      expect(within(todoColumnBefore).getByText("Design homepage")).toBeInTheDocument();

      // Trigger drag end event
      await act(async () => {
        if (capturedOnDragEnd) {
          const result = capturedOnDragEnd({
            active: { id: "task-1" },
            over: { id: TaskStatus.IN_PROGRESS },
          });
          if (result instanceof Promise) {
            await result;
          }
        }
      });

      // Wait for rollback to occur
      await waitFor(() => {
        // After rollback, task should be back in original column
        const todoColumnAfter = screen.getByTestId("column-NOT_STARTED");
        expect(within(todoColumnAfter).getByText("Design homepage")).toBeInTheDocument();
      });

      // Verify callback was NOT called due to error
      expect(mockOnStatusChange).not.toHaveBeenCalled();

      consoleErrorSpy.mockRestore();
    });

    it("should show error toast notification when API call fails", async (): Promise<void> => {
      const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

      // Mock API failure
      mockApiPatch.mockRejectedValueOnce(new Error("Server error"));

      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Trigger drag end event
      await act(async () => {
        if (capturedOnDragEnd) {
          const result = capturedOnDragEnd({
            active: { id: "task-1" },
            over: { id: TaskStatus.IN_PROGRESS },
          });
          if (result instanceof Promise) {
            await result;
          }
        }
      });

      // Wait for error handling
      await waitFor(() => {
        // Verify showToast was called with error message
        expect(mockShowToast).toHaveBeenCalledWith(
          "Unable to update task status. Please try again.",
          "error"
        );
      });

      consoleErrorSpy.mockRestore();
    });

    it("should not make API call when dropping on same column", async (): Promise<void> => {
      const fetchMock = global.fetch as ReturnType<typeof vi.fn>;

      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Trigger drag end event with same status
      await act(async () => {
        if (capturedOnDragEnd) {
          const result = capturedOnDragEnd({
            active: { id: "task-1" },
            over: { id: TaskStatus.NOT_STARTED }, // Same as task's current status
          });
          if (result instanceof Promise) {
            await result;
          }
        }
      });

      // No API call should be made
      expect(fetchMock).not.toHaveBeenCalled();
      // No callback should be called
      expect(mockOnStatusChange).not.toHaveBeenCalled();
    });

    it("should handle drag cancel (no drop target)", async (): Promise<void> => {
      const fetchMock = global.fetch as ReturnType<typeof vi.fn>;

      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      // Trigger drag end event with no drop target
      await act(async () => {
        if (capturedOnDragEnd) {
          const result = capturedOnDragEnd({
            active: { id: "task-1" },
            over: null,
          });
          if (result instanceof Promise) {
            await result;
          }
        }
      });

      // Task should remain in original column
      const todoColumn = screen.getByTestId("column-NOT_STARTED");
      expect(within(todoColumn).getByText("Design homepage")).toBeInTheDocument();

      // No API call should be made
      expect(fetchMock).not.toHaveBeenCalled();
    });
  });

  describe("Accessibility", (): void => {
    it("should have proper heading hierarchy", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      const h3Headings = screen.getAllByRole("heading", { level: 3 });
      expect(h3Headings.length).toBe(4);
    });

    it("should have keyboard-navigable task cards", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      const taskCards = screen.getAllByRole("article");
      expect(taskCards.length).toBe(mockTasks.length);
    });

    it("should announce column changes to screen readers", (): void => {
      render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

      const columns = screen.getAllByRole("region");
      expect(columns.length).toBeGreaterThan(0);
      columns.forEach((column) => {
        expect(column).toHaveAttribute("aria-label");
      });
    });
  });

  describe("Responsive Design", (): void => {
    it("should apply responsive grid classes", (): void => {
      const { container } = render(
        <KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />
      );

      const boardGrid = container.querySelector('[data-testid="kanban-grid"]');
      expect(boardGrid).toBeInTheDocument();
      const className = boardGrid?.className ?? "";
      expect(className).toMatch(/grid/);
    });
  });
});
