import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { RoutesPage } from "../pages/Routes.tsx";

// Leaflet does not render in jsdom — replace the lazily-loaded map with a stub
// that just reports the stop count so we can assert it received the route data.
vi.mock("../components/RouteMap.js", () => ({
  default: ({ stops }: { stops: unknown[] }) => (
    <div data-testid="route-map">map:{stops.length}</div>
  ),
}));

const MANAGER = { id: "m1", name: "Manager", role: "manager", active: true };
const GROOMER = { id: "g1", name: "Sam Groomer", role: "groomer", active: true };

const ROUTE_RESPONSE = {
  route: {
    id: "r1",
    staffId: "g1",
    routeDate: "2026-06-09",
    status: "optimized",
    totalTravelMins: 95,
    totalDistanceKm: "42.50",
  },
  stops: [
    {
      id: "s1",
      appointmentId: "a1",
      stopOrder: 1,
      latitude: 51.5,
      longitude: -0.1,
      travelMinsFromPrev: null,
      travelDistanceKmFromPrev: null,
      bufferMins: 15,
      appointmentStartTime: "2026-06-09T09:00:00.000Z",
      appointmentEndTime: "2026-06-09T10:00:00.000Z",
      appointmentStatus: "confirmed",
      clientId: "c1",
      clientName: "Alice",
      clientAddress: "1 High St",
      conflict: { hasConflict: false },
    },
    {
      id: "s2",
      appointmentId: "a2",
      stopOrder: 2,
      latitude: 51.52,
      longitude: -0.12,
      travelMinsFromPrev: 20,
      travelDistanceKmFromPrev: "8.00",
      bufferMins: 15,
      appointmentStartTime: "2026-06-09T11:00:00.000Z",
      appointmentEndTime: "2026-06-09T12:00:00.000Z",
      appointmentStatus: "confirmed",
      clientId: "c2",
      clientName: "Bob",
      clientAddress: "2 Low St",
      conflict: { hasConflict: true },
    },
  ],
  hasConflicts: true,
  conflictCount: 1,
};

function mockFetch(meRole: "manager" | "groomer") {
  return vi.fn((url: string, opts?: RequestInit) => {
    if (url === "/api/staff/me") {
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve(meRole === "manager" ? MANAGER : GROOMER),
      } as Response);
    }
    if (url === "/api/staff") {
      return Promise.resolve({ ok: true, json: () => Promise.resolve([MANAGER, GROOMER]) } as Response);
    }
    if (url.startsWith("/api/routes/daily")) {
      return Promise.resolve({ ok: true, json: () => Promise.resolve(ROUTE_RESPONSE) } as Response);
    }
    if (url === "/api/routes/optimize" && opts?.method === "POST") {
      return Promise.resolve({ ok: true, json: () => Promise.resolve(ROUTE_RESPONSE) } as Response);
    }
    if (/^\/api\/routes\/[^/]+\/reorder$/.test(url) && opts?.method === "PATCH") {
      return Promise.resolve({ ok: true, json: () => Promise.resolve(ROUTE_RESPONSE) } as Response);
    }
    if (/^\/api\/routes\/[^/]+\/export\/google-maps$/.test(url)) {
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ platform: "google-maps", url: "https://www.google.com/maps/dir/?api=1", stopCount: 2, waypointCount: 0 }),
      } as Response);
    }
    if (/^\/api\/routes\/[^/]+\/export\/apple-maps$/.test(url)) {
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ platform: "apple-maps", url: "maps://?saddr=51.5,-0.1&daddr=51.52,-0.12", stopCount: 2, waypointCount: 0 }),
      } as Response);
    }
    return Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as Response);
  });
}

beforeEach(() => {
  vi.restoreAllMocks();
});

describe("RoutesPage", () => {
  it("renders stop cards, summary, status badge and map for a manager", async () => {
    global.fetch = mockFetch("manager") as unknown as typeof fetch;
    render(<RoutesPage />);

    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
    expect(screen.getByText("Bob")).toBeInTheDocument();
    expect(screen.getByText("1 High St")).toBeInTheDocument();

    // Summary: travel time formatted, distance shown
    expect(screen.getByText("1 h 35 min")).toBeInTheDocument();
    expect(screen.getByText("42.50 km")).toBeInTheDocument();

    // Status badge
    expect(screen.getByText("Optimized")).toBeInTheDocument();

    // First stop is start-of-route; second shows travel from previous
    expect(screen.getByText("Start of route")).toBeInTheDocument();
    expect(screen.getByText("20 min travel from previous")).toBeInTheDocument();

    // Map received both stops (lazy chunk resolves asynchronously)
    expect(await screen.findByTestId("route-map")).toHaveTextContent("map:2");
  });

  it("shows the groomer selector for managers", async () => {
    global.fetch = mockFetch("manager") as unknown as typeof fetch;
    render(<RoutesPage />);
    await waitFor(() => expect(screen.getByText("Groomer")).toBeInTheDocument());
  });

  it("hides the groomer selector for groomer role (auto-filtered)", async () => {
    global.fetch = mockFetch("groomer") as unknown as typeof fetch;
    render(<RoutesPage />);
    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
    expect(screen.queryByText("Groomer")).not.toBeInTheDocument();
  });

  it("renders a drag handle for each stop (drag-to-reorder enabled)", async () => {
    global.fetch = mockFetch("manager") as unknown as typeof fetch;
    render(<RoutesPage />);

    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
    expect(screen.getByLabelText("Drag to reorder Alice")).toBeInTheDocument();
    expect(screen.getByLabelText("Drag to reorder Bob")).toBeInTheDocument();
  });

  it("flags the tight-schedule conflict on the affected stop", async () => {
    global.fetch = mockFetch("manager") as unknown as typeof fetch;
    render(<RoutesPage />);

    await waitFor(() => expect(screen.getByText("Bob")).toBeInTheDocument());
    expect(
      screen.getByText(/Tight schedule — travel \+ buffer may exceed the gap/)
    ).toBeInTheDocument();
  });

  it("does not show the re-optimize hint before any manual reorder", async () => {
    global.fetch = mockFetch("manager") as unknown as typeof fetch;
    render(<RoutesPage />);

    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
    expect(screen.queryByText("Re-optimize")).not.toBeInTheDocument();
  });

  it("calls the optimize endpoint when Optimize is clicked", async () => {
    const fetchMock = mockFetch("manager");
    global.fetch = fetchMock as unknown as typeof fetch;
    render(<RoutesPage />);

    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
    fireEvent.click(screen.getByText("Optimize"));

    await waitFor(() =>
      expect(fetchMock).toHaveBeenCalledWith(
        "/api/routes/optimize",
        expect.objectContaining({ method: "POST" })
      )
    );
  });

  it("renders both navigation export buttons when the route has stops", async () => {
    global.fetch = mockFetch("manager") as unknown as typeof fetch;
    render(<RoutesPage />);

    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());
    expect(screen.getByRole("button", { name: "Open in Google Maps" })).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Open in Apple Maps" })).toBeInTheDocument();
  });

  it("fetches the export deep link and opens it when Open in Google Maps is clicked", async () => {
    const fetchMock = mockFetch("manager");
    global.fetch = fetchMock as unknown as typeof fetch;
    const openSpy = vi
      .spyOn(window, "open")
      .mockReturnValue({ location: { href: "" }, close: vi.fn() } as unknown as Window);

    render(<RoutesPage />);
    await waitFor(() => expect(screen.getByText("Alice")).toBeInTheDocument());

    fireEvent.click(screen.getByRole("button", { name: "Open in Google Maps" }));

    await waitFor(() =>
      expect(fetchMock).toHaveBeenCalledWith("/api/routes/r1/export/google-maps")
    );
    expect(openSpy).toHaveBeenCalled();
  });
});
