import Keybindings from "../../../src/js/modules/Keybindings/Keybindings";

describe("Keybindings module", () => {
    /** @type {Keybindings} */
    let keybindingsMod;
    let mockTable;
    
    beforeEach(() => {
        // Create mock optionsList
        const mockOptionsList = {
            register: jest.fn(),
            generate: jest.fn().mockImplementation((defaults, options) => {
                return { ...defaults, ...options };
            })
        };
        
        // Create mock eventBus
        const mockEventBus = {
            subscribe: jest.fn(),
            unsubscribe: jest.fn(),
            subscribed: jest.fn(),
            subscriptionChange: jest.fn(),
            dispatch: jest.fn(),
            chain: jest.fn(),
            confirm: jest.fn()
        };
        
        // Create mock externalEvents
        const mockExternalEvents = {
            dispatch: jest.fn(),
            subscribed: jest.fn(),
            subscriptionChange: jest.fn()
        };
        
        // Create a simplified mock of the table
        mockTable = {
            options: {
                keybindings: false // Disable default keybindings for most tests
            },
            element: {
                addEventListener: jest.fn(),
                removeEventListener: jest.fn(),
                focus: jest.fn()
            },
            rowManager: {
                element: {
                    clientHeight: 200,
                    scrollHeight: 600,
                    scrollTop: 200
                },
                scrollToRow: jest.fn(),
                getDisplayRows: jest.fn().mockReturnValue([
                    { id: 1 }, 
                    { id: 2 }, 
                    { id: 3 }
                ]),
                displayRowsCount: 3
            },
            columnManager: {
                optionsList: mockOptionsList
            },
            optionsList: mockOptionsList,
            eventBus: mockEventBus,
            externalEvents: mockExternalEvents,
            registerTableFunction: jest.fn(),
            initGuard: jest.fn()
        };
        
        // Mock the prototype methods of the Module class
        jest.spyOn(Keybindings.prototype, 'registerTableOption').mockImplementation(function(key, value) {
            this.table.optionsList.register(key, value);
        });
        
        jest.spyOn(Keybindings.prototype, 'registerColumnOption').mockImplementation(function(key, value) {
            this.table.columnManager.optionsList.register(key, value);
        });
        
        // Mock subscribe method which is used in initialize
        jest.spyOn(Keybindings.prototype, 'subscribe').mockImplementation(function(key, callback) {
            return this.table.eventBus.subscribe(key, callback);
        });
        
        // Create an instance of the Keybindings module with the mock table
        keybindingsMod = new Keybindings(mockTable);
        
        // Mock the dispatch method
        keybindingsMod.dispatch = jest.fn();
        
        // Mock registerTableFunction
        keybindingsMod.registerTableFunction = function(name, callback) {
            mockTable.registerTableFunction(name, callback);
        };
        
        // Initialize the module
        keybindingsMod.initialize();
    });
    
    afterEach(() => {
        jest.clearAllMocks();
        jest.restoreAllMocks();
    });

	it("should register keybindings table option during construction", () => {
		const testActionFunc = jest.fn();
        const mod = new Keybindings({
			...mockTable,
			options: {
				keybindings: {
					testAction: "ctrl + q"
				}
			},
		});
		Object.assign(Keybindings.actions, {
            testAction: testActionFunc,
		})
		mod.initialize();
		mod.keydownBinding(new KeyboardEvent("keydown", { key: "q", ctrlKey: true }));
		mod.keyupBinding(new KeyboardEvent("keyup", { key: "q", ctrlKey: true }));
		expect(testActionFunc).toHaveBeenCalled();
	})
    
    it("should initialize watchKeys and pressedKeys as empty when keybindings disabled", () => {
        // With keybindings:false, the module should initialize empty structures
        expect(keybindingsMod.watchKeys).toEqual({});
        expect(keybindingsMod.pressedKeys).toEqual([]);
    });

    it("should map default bindings when keybindings is enabled", () => {
        // Create a new instance with enabled keybindings
        mockTable.options.keybindings = {};
        keybindingsMod = new Keybindings(mockTable);
        keybindingsMod.dispatch = jest.fn();
        
        // Mock the mapBinding method to inspect what it's called with
        keybindingsMod.mapBinding = jest.fn();
        
        keybindingsMod.initialize();
        
        // Verify that mapBinding was called at least once
        expect(keybindingsMod.mapBinding).toHaveBeenCalled();
    });
    
    it("should map bindings through the public API", () => {
        // Create a separate instance to ensure clean state
        const instance = new Keybindings({...mockTable, options: {keybindings: false}});
        
        // Initialize the instance to set up watchKeys
        instance.initialize();
        
        // Add a spy to track key binding objects
        let capturedBindings = [];
        const originalMapBinding = instance.mapBinding;
        instance.mapBinding = jest.fn((action, binding) => {
            capturedBindings.push({action, binding});
            return originalMapBinding.call(instance, action, binding);
        });
        
        // Map a simple binding
        const bindings = {
            scrollToStart: "ctrl + home"
        };
        
        // Call the method
        instance.mapBindings(bindings);
        
        // Verify mapBinding was called with expected parameters
        expect(instance.mapBinding).toHaveBeenCalledWith("scrollToStart", "ctrl + home");
        expect(capturedBindings.length).toBeGreaterThan(0);
        
        // Verify the first binding was for scrollToStart
        const capturedBinding = capturedBindings[0];
        expect(capturedBinding.action).toBe("scrollToStart");
        expect(capturedBinding.binding).toBe("ctrl + home");
    });
    
    it("should handle key combinations correctly", () => {
        // Mock a key binding
        const binding = {
            action: jest.fn(),
            keys: [38], // UP arrow
            ctrl: true,
            shift: false,
            meta: false
        };
        
        // Create a mock event
        const event = {
            ctrlKey: true,
            shiftKey: false,
            metaKey: false
        };
        
        // Set pressed keys
        keybindingsMod.pressedKeys = [38];
        
        // Test binding check - should match
        const result = keybindingsMod.checkBinding(event, binding);
        expect(result).toBe(true);
        expect(binding.action).toHaveBeenCalled();
        
        // Test with non-matching modifiers
        event.shiftKey = true;
        const nonMatchResult = keybindingsMod.checkBinding(event, binding);
        expect(nonMatchResult).toBe(false);
    });
    
    it("should handle clearing bindings", () => {
        // Set up the binding references
        keybindingsMod.keyupBinding = jest.fn();
        keybindingsMod.keydownBinding = jest.fn();
        
        // Call clear bindings
        keybindingsMod.clearBindings();
        
        // Check that event listeners were removed
        expect(keybindingsMod.table.element.removeEventListener).toHaveBeenCalledWith("keydown", keybindingsMod.keyupBinding);
        expect(keybindingsMod.table.element.removeEventListener).toHaveBeenCalledWith("keyup", keybindingsMod.keydownBinding);
    });
    
    it("should execute scrollPageUp action by calling scrollToRow for first row when scroll would be negative", () => {
        // Create mock event
        const event = {
            preventDefault: jest.fn()
        };
        
        // Change scrollTop to a small value to trigger the "else" branch
        keybindingsMod.table.rowManager.element.scrollTop = 100;
        keybindingsMod.table.rowManager.element.clientHeight = 200;
        
        // Execute the action directly to test the scrollToRow path
        Keybindings.actions.scrollPageUp.call(keybindingsMod, event);
        
        // Check scrollToRow was called with first row
        expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 1 });
        expect(event.preventDefault).toHaveBeenCalled();
        expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
    });
    
    it("should execute scrollPageDown action by calling scrollToRow for last row", () => {
        // Create mock event
        const event = {
            preventDefault: jest.fn()
        };
        
        // Set scrollTop and scrollHeight to trigger the "else" branch
        keybindingsMod.table.rowManager.element.scrollTop = 500;
        keybindingsMod.table.rowManager.element.clientHeight = 200;
        keybindingsMod.table.rowManager.element.scrollHeight = 600;
        
        // Execute the action directly
        Keybindings.actions.scrollPageDown.call(keybindingsMod, event);
        
        // Check that the right methods were called
        expect(event.preventDefault).toHaveBeenCalled();
        expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
        
        // Check scrollToRow was called with last row since we're at the bottom
        expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 3 });
    });
    
    it("should execute scrollToStart action correctly", () => {
        // Create mock event
        const event = {
            preventDefault: jest.fn()
        };
        
        // Execute the action
        Keybindings.actions.scrollToStart.call(keybindingsMod, event);
        
        // Check that event was prevented
        expect(event.preventDefault).toHaveBeenCalled();
        
        // Check that focus was called
        expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
        
        // Check scrollToRow was called with first row
        expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 1 });
    });
    
    it("should execute scrollToEnd action correctly", () => {
        // Create mock event
        const event = {
            preventDefault: jest.fn()
        };
        
        // Execute the action
        Keybindings.actions.scrollToEnd.call(keybindingsMod, event);
        
        // Check that event was prevented
        expect(event.preventDefault).toHaveBeenCalled();
        
        // Check that focus was called
        expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
        
        // Check scrollToRow was called with last row
        expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 3 });
    });
    
    it("should execute keyBlock action correctly", () => {
        // Create mock event
        const event = {
            stopPropagation: jest.fn(),
            preventDefault: jest.fn()
        };
        
        // Execute the action
        Keybindings.actions.keyBlock.call(keybindingsMod, event);
        
        // Check that event methods were called
        expect(event.stopPropagation).toHaveBeenCalled();
        expect(event.preventDefault).toHaveBeenCalled();
    });
    
    it("should execute navigation actions correctly", () => {
        // Test all navigation actions
        const actions = ["navPrev", "navNext", "navUp", "navDown", "navLeft", "navRight"];
        const events = {};
        
        actions.forEach(action => {
            events[action] = { };
            Keybindings.actions[action].call(keybindingsMod, events[action]);
            expect(keybindingsMod.dispatch).toHaveBeenCalledWith(`keybinding-${action.replace("nav", "nav-").toLowerCase()}`, events[action]);
        });
    });
});
