import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MemoryRouter } from "react-router-dom";

const { navigateMock } = vi.hoisted(() => ({
    navigateMock: vi.fn(),
}));

const {
    listSessionsMock,
    deleteSessionMock,
    renameSessionMock,
    showRecordingOverlayMock,
    exportSessionPackageMock,
    importSessionPackageMock,
    revealInFileManagerMock,
    getSessionMetadataMock,
} = vi.hoisted(() => ({
    listSessionsMock: vi.fn(),
    deleteSessionMock: vi.fn(),
    renameSessionMock: vi.fn(),
    showRecordingOverlayMock: vi.fn(),
    exportSessionPackageMock: vi.fn(),
    importSessionPackageMock: vi.fn(),
    revealInFileManagerMock: vi.fn(),
    getSessionMetadataMock: vi.fn(),
}));

const { scanSessionForSensitiveDataMock } = vi.hoisted(() => ({
    scanSessionForSensitiveDataMock: vi.fn(),
}));

const { getWorkflowMetricsMock, initializeWorkflowMetricsStoreMock, trackWorkflowEventMock } =
    vi.hoisted(() => ({
        getWorkflowMetricsMock: vi.fn(),
        initializeWorkflowMetricsStoreMock: vi.fn(),
        trackWorkflowEventMock: vi.fn(),
    }));

const { registerCategoryMock, unregisterCategoryMock, setShowHelpMock } = vi.hoisted(() => ({
    registerCategoryMock: vi.fn(),
    unregisterCategoryMock: vi.fn(),
    setShowHelpMock: vi.fn(),
}));

vi.mock("react-router-dom", async (importOriginal) => {
    const actual = await importOriginal<typeof import("react-router-dom")>();
    return {
        ...actual,
        useNavigate: () => navigateMock,
    };
});

vi.mock("../lib/api", () => ({
    listSessions: listSessionsMock,
    deleteSession: deleteSessionMock,
    renameSession: renameSessionMock,
    showRecordingOverlay: showRecordingOverlayMock,
    exportSessionPackage: exportSessionPackageMock,
    importSessionPackage: importSessionPackageMock,
    revealInFileManager: revealInFileManagerMock,
    getSessionMetadata: getSessionMetadataMock,
    onProcessingStarted: vi.fn().mockResolvedValue(() => {}),
    onProcessingComplete: vi.fn().mockResolvedValue(() => {}),
    onProcessingError: vi.fn().mockResolvedValue(() => {}),
}));

vi.mock("../lib/privacyScan", () => ({
    scanSessionForSensitiveData: scanSessionForSensitiveDataMock,
}));

vi.mock("../lib/usageMetrics", () => ({
    getWorkflowMetrics: getWorkflowMetricsMock,
    initializeWorkflowMetricsStore: initializeWorkflowMetricsStoreMock,
    trackWorkflowEvent: trackWorkflowEventMock,
}));

vi.mock("../contexts/ShortcutContext", () => ({
    useShortcutContext: () => ({
        registerCategory: registerCategoryMock,
        unregisterCategory: unregisterCategoryMock,
        setShowHelp: setShowHelpMock,
    }),
}));

vi.mock("../components/ShareDialog", () => ({
    default: ({ sessionId }: { sessionId: string }) => <div>Share modal for {sessionId}</div>,
}));

import LibraryView from "./LibraryView";

type SessionSummary = {
    session_id: string;
    title: string | null;
    created_at: string;
    duration_ms: number | null;
    step_count: number;
    thumbnail_path: string | null;
    has_terminal: boolean;
    git_branch: string | null;
    git_repo: string | null;
};

function renderView() {
    return render(
        <MemoryRouter>
            <LibraryView />
        </MemoryRouter>
    );
}

const SESSIONS: SessionSummary[] = [
    {
        session_id: "session-aaa111",
        title: "Checkout flow",
        created_at: "1735689600",
        duration_ms: 65000,
        step_count: 3,
        thumbnail_path: null,
        has_terminal: true,
        git_branch: "main",
        git_repo: "repo-a",
    },
    {
        session_id: "session-bbb222",
        title: "Settings page",
        created_at: "1735603200",
        duration_ms: 120000,
        step_count: 1,
        thumbnail_path: null,
        has_terminal: false,
        git_branch: null,
        git_repo: null,
    },
];

describe("LibraryView", () => {
    beforeEach(() => {
        vi.useRealTimers();
        navigateMock.mockReset();
        listSessionsMock.mockReset();
        deleteSessionMock.mockReset();
        renameSessionMock.mockReset();
        showRecordingOverlayMock.mockReset();
        exportSessionPackageMock.mockReset();
        importSessionPackageMock.mockReset();
        revealInFileManagerMock.mockReset();
        getSessionMetadataMock.mockReset();
        scanSessionForSensitiveDataMock.mockReset();
        getWorkflowMetricsMock.mockReset();
        initializeWorkflowMetricsStoreMock.mockReset();
        trackWorkflowEventMock.mockReset();
        registerCategoryMock.mockReset();
        unregisterCategoryMock.mockReset();
        setShowHelpMock.mockReset();

        getWorkflowMetricsMock.mockReturnValue({
            windowDays: 30,
            recordingsStarted: 0,
            sessionsShared: 0,
            shareCompletionRate: 0,
            medianRecordToShareSeconds: null,
            medianEditsPerSharedSession: null,
        });
        initializeWorkflowMetricsStoreMock.mockResolvedValue(undefined);
        exportSessionPackageMock.mockResolvedValue("/tmp/session-package");
        revealInFileManagerMock.mockResolvedValue(undefined);
        importSessionPackageMock.mockResolvedValue({
            session_id: "session-imported",
            source_session_id: "source-session",
            imported_files: 4,
        });
        getSessionMetadataMock.mockResolvedValue({
            session_id: "session-aaa111",
            created_at: "1735689600",
            display: { id: "display-1", resolution: "1920x1080", refresh_rate: 60 },
            duration_ms: 1200,
            event_count: 12,
            recording_path: "/tmp/recording.mp4",
            events_path: "/tmp/events.ndjson",
            title: "Checkout flow",
            thumbnail_path: null,
            has_terminal: false,
            git_branch: null,
            git_commit: null,
        });
        scanSessionForSensitiveDataMock.mockResolvedValue({
            scannedPaths: ["/tmp/events.ndjson"],
            findings: [],
            highSeverityCount: 0,
            mediumSeverityCount: 0,
        });
    });

    afterEach(() => {
        cleanup();
    });

    it("renders sessions and filters by search query", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);

        renderView();

        expect(screen.getByText("Loading sessions...")).toBeTruthy();
        expect(await screen.findByText("Checkout flow")).toBeTruthy();
        expect(screen.getByText("Settings page")).toBeTruthy();

        const input = screen.getByPlaceholderText("Search");
        fireEvent.change(input, { target: { value: "checkout" } });

        expect(screen.getByText("Checkout flow")).toBeTruthy();
        expect(screen.queryByText("Settings page")).toBeNull();
    });

    it("shows error banner when sessions fail to load", async () => {
        listSessionsMock.mockRejectedValue(new Error("boom"));

        renderView();

        expect(await screen.findByText("Error: boom")).toBeTruthy();
    });

    it("shows no-results state for unmatched search", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);
        renderView();

        expect(await screen.findByText("Checkout flow")).toBeTruthy();

        fireEvent.change(screen.getByPlaceholderText("Search"), {
            target: { value: "not-found" },
        });

        expect(screen.getByText("No sessions found")).toBeTruthy();
        expect(screen.getByText("Try a different search term")).toBeTruthy();
    });

    it("starts rename flow and saves on Enter", async () => {
        listSessionsMock
            .mockResolvedValueOnce(SESSIONS)
            .mockResolvedValueOnce([{ ...SESSIONS[0], title: "Checkout flow v2" }]);
        renameSessionMock.mockResolvedValue(undefined);

        renderView();

        expect(await screen.findByText("Checkout flow")).toBeTruthy();

        fireEvent.click(screen.getByText("Checkout flow"));
        const editInput = screen.getByDisplayValue("Checkout flow");
        fireEvent.change(editInput, { target: { value: "Checkout flow v2" } });
        fireEvent.keyDown(editInput, { key: "Enter" });

        await waitFor(() => {
            expect(renameSessionMock).toHaveBeenCalledWith("session-aaa111", "Checkout flow v2");
        });
        expect(listSessionsMock).toHaveBeenCalledTimes(2);
    });

    it("cancels rename when title is empty", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);
        renderView();

        expect(await screen.findByText("Checkout flow")).toBeTruthy();
        fireEvent.click(screen.getByText("Checkout flow"));

        const editInput = screen.getByDisplayValue("Checkout flow");
        fireEvent.change(editInput, { target: { value: "   " } });
        fireEvent.keyDown(editInput, { key: "Enter" });

        await waitFor(() => {
            expect(screen.getByText("Checkout flow")).toBeTruthy();
        });
        expect(renameSessionMock).not.toHaveBeenCalled();
    });

    it("deletes a session and reloads list", async () => {
        listSessionsMock.mockResolvedValueOnce(SESSIONS).mockResolvedValueOnce([SESSIONS[1]]);
        deleteSessionMock.mockResolvedValue(undefined);

        renderView();

        expect(await screen.findByText("Checkout flow")).toBeTruthy();
        fireEvent.click(screen.getAllByTitle("Delete")[0]);

        await waitFor(() => {
            expect(deleteSessionMock).toHaveBeenCalledWith("session-aaa111");
        });
        expect(listSessionsMock).toHaveBeenCalledTimes(2);
    });

    it("logs when delete fails", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);
        deleteSessionMock.mockRejectedValue(new Error("delete failed"));
        const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);

        renderView();

        expect(await screen.findByText("Checkout flow")).toBeTruthy();
        fireEvent.click(screen.getAllByTitle("Delete")[0]);

        await waitFor(() => {
            expect(consoleSpy).toHaveBeenCalled();
        });
        expect(listSessionsMock).toHaveBeenCalledTimes(1);
        consoleSpy.mockRestore();
    });

    it("triggers share and new recording actions", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);

        renderView();

        expect(await screen.findByText("Checkout flow")).toBeTruthy();

        fireEvent.click(screen.getByRole("button", { name: /create/i }));
        expect(showRecordingOverlayMock).toHaveBeenCalledTimes(1);

        fireEvent.click(screen.getAllByTitle("Share")[0]);
        expect(screen.getByText("Share modal for session-aaa111")).toBeTruthy();
    });

    it("blocks export when privacy scan finds secrets and allows explicit override", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);
        scanSessionForSensitiveDataMock.mockResolvedValueOnce({
            scannedPaths: ["/tmp/events.ndjson"],
            findings: [
                {
                    rule: "OpenAI API key",
                    severity: "high",
                    path: "/tmp/events.ndjson",
                    snippet: "... sk-abcdef12345678901234567890 ...",
                },
            ],
            highSeverityCount: 1,
            mediumSeverityCount: 0,
        });

        renderView();
        expect(await screen.findByText("Checkout flow")).toBeTruthy();

        fireEvent.click(screen.getAllByTitle("Export package")[0]);

        await waitFor(() => {
            expect(getSessionMetadataMock).toHaveBeenCalledWith("session-aaa111");
        });
        await waitFor(() => {
            expect(scanSessionForSensitiveDataMock).toHaveBeenCalled();
        });
        expect(exportSessionPackageMock).not.toHaveBeenCalled();
        expect(screen.getByText(/Privacy check flagged potential secrets/i)).toBeTruthy();

        fireEvent.click(screen.getByRole("button", { name: /export package anyway/i }));

        await waitFor(() => {
            expect(exportSessionPackageMock).toHaveBeenCalledWith("session-aaa111");
        });
        await waitFor(() => {
            expect(revealInFileManagerMock).toHaveBeenCalledWith("/tmp/session-package");
        });
    });

    it("registers and unregisters navigation shortcuts", async () => {
        listSessionsMock.mockResolvedValue(SESSIONS);

        const view = renderView();
        expect(await screen.findByText("Checkout flow")).toBeTruthy();

        expect(registerCategoryMock).toHaveBeenCalledWith(
            expect.objectContaining({ name: "Navigation" })
        );

        view.unmount();
        expect(unregisterCategoryMock).toHaveBeenCalledWith("Navigation");
    });
});
