#!/usr/bin/env node
/**
 * Lightweight Test Runner for Entity Selector Validation
 * No heavy dependencies - runs directly with Node.js
 */

const { JSDOM } = require('jsdom');
const fs = require('fs');
const path = require('path');

// Setup DOM environment with script execution enabled
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
    url: 'http://localhost',
    runScripts: 'dangerously',
    resources: 'usable'
});

const window = dom.window;
const document = window.document;

// Initialize mixin namespace before loading scripts
window._EntitySelectorMixins = {};

// Load jQuery using window.eval to properly share context
const jqueryCode = fs.readFileSync(
    path.join(__dirname, '../node_modules/jquery/dist/jquery.min.js'),
    'utf8'
);
window.eval(jqueryCode);

// Make $ available in Node context
const $ = window.jQuery;

// Load and execute the validation mixin using window.eval
const validationCode = fs.readFileSync(
    path.join(__dirname, '../sources/js/admin/entity-selector/_validation.js'),
    'utf8'
);
window.eval(validationCode);

// Get the validation mixin
const validationMixin = window._EntitySelectorMixins.validation;

// Test utilities
let passed = 0;
let failed = 0;
let currentGroup = '';

function describe(name, fn) {
    currentGroup = name;
    console.log(`\n\x1b[1m${name}\x1b[0m`);
    fn();
}

function test(name, fn) {
    try {
        fn();
        passed++;
        console.log(`  \x1b[32m✓\x1b[0m ${name}`);
    } catch (e) {
        failed++;
        console.log(`  \x1b[31m✗\x1b[0m ${name}`);
        console.log(`    \x1b[31m${e.message}\x1b[0m`);
    }
}

function expect(actual) {
    return {
        toBe(expected) {
            if (actual !== expected) {
                throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
            }
        },
        toEqual(expected) {
            if (JSON.stringify(actual) !== JSON.stringify(expected)) {
                throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
            }
        },
        toContain(expected) {
            if (typeof actual === 'string') {
                if (!actual.includes(expected)) {
                    throw new Error(`Expected "${actual}" to contain "${expected}"`);
                }
            } else if (Array.isArray(actual)) {
                if (!actual.includes(expected)) {
                    throw new Error(`Expected array to contain ${expected}`);
                }
            }
        },
        toBeDefined() {
            if (actual === undefined) {
                throw new Error('Expected value to be defined');
            }
        }
    };
}

// Test fixtures
function createValidator() {
    const $wrapper = $(`
        <div class="entity-selector-wrapper">
            <div class="target-block" data-block-type="categories">
                <div class="selection-group" data-group-index="0">
                    <div class="group-include">
                        <div class="value-picker include-picker">
                            <div class="entity-chips"></div>
                        </div>
                    </div>
                    <div class="exclude-rows-container">
                        <div class="exclude-row" data-exclude-index="0">
                            <div class="value-picker exclude-picker">
                                <div class="entity-chips"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    `);

    $(document.body).empty().append($wrapper);

    return Object.assign({}, validationMixin, {
        $wrapper: $wrapper,
        $dropdown: $('<div class="target-search-dropdown"></div>'),
        config: { trans: {} },
        activeGroup: {
            blockType: 'categories',
            groupIndex: 0,
            section: 'include',
            excludeIndex: 0,
            searchEntity: 'categories'
        },
        treeFlatData: null,
        escapeHtml: (str) => str,
        escapeAttr: (str) => str
    });
}

function createMockChips($picker, ids) {
    const $chips = $picker.find('.entity-chips');
    $chips.empty();
    ids.forEach(id => {
        $chips.append(`<span class="entity-chip" data-id="${id}"></span>`);
    });
}

// =========================================================================
// TESTS
// =========================================================================

console.log('\n\x1b[36m╔════════════════════════════════════════════════════════════╗\x1b[0m');
console.log('\x1b[36m║       Entity Selector Validation Test Suite                 ║\x1b[0m');
console.log('\x1b[36m╚════════════════════════════════════════════════════════════╝\x1b[0m');

// GROUP 1: Include/Exclude Conflict Detection
describe('Include/Exclude Conflict Detection', () => {

    test('TC-001: Block adding to EXCLUDE when entity is in INCLUDE', () => {
        const validator = createValidator();
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [5]);

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(5, 'Electronics', 'exclude', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('include_exclude_conflict');
        expect(result.error).toContain('already in the include list');
    });

    test('TC-002: Block adding to INCLUDE when entity is in EXCLUDE', () => {
        const validator = createValidator();
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [5]);

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(5, 'Electronics', 'include', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('include_exclude_conflict');
        expect(result.error).toContain('already in the exclude list');
    });

    test('TC-003: Allow adding to INCLUDE when entity is NOT in EXCLUDE', () => {
        const validator = createValidator();
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [10]);

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(5, 'Electronics', 'include', {});

        expect(result.valid).toBe(true);
    });

    test('TC-004: Allow adding to EXCLUDE when entity is NOT in INCLUDE', () => {
        const validator = createValidator();
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [10]);

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(5, 'Electronics', 'exclude', {});

        expect(result.valid).toBe(true);
    });

    test('TC-005: Check across multiple exclude rows', () => {
        const validator = createValidator();
        const $container = validator.$wrapper.find('.exclude-rows-container');
        $container.append(`
            <div class="exclude-row" data-exclude-index="1">
                <div class="value-picker exclude-picker">
                    <div class="entity-chips">
                        <span class="entity-chip" data-id="5"></span>
                    </div>
                </div>
            </div>
        `);

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(5, 'Electronics', 'include', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('include_exclude_conflict');
    });
});

// GROUP 2: Redundant Selection Detection
describe('Redundant Selection Detection', () => {

    test('TC-010: Block duplicate selection in INCLUDE', () => {
        const validator = createValidator();
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [5]);

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(5, 'Electronics', 'include', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('redundant');
        expect(result.error).toContain('already selected');
    });

    test('TC-011: Block duplicate selection in EXCLUDE', () => {
        const validator = createValidator();
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [5]);

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(5, 'Electronics', 'exclude', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('redundant');
        expect(result.error).toContain('already in an exclude list');
    });

    test('TC-012: Allow selection when not duplicate', () => {
        const validator = createValidator();
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [10]);

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(5, 'Electronics', 'include', {});

        expect(result.valid).toBe(true);
    });
});

// GROUP 3: Parent-Child Conflict Detection
describe('Parent-Child Conflict Detection', () => {

    const treeData = [
        { id: 1, name: 'Electronics', parent_id: 0 },
        { id: 2, name: 'Computers', parent_id: 1 },
        { id: 3, name: 'Phones', parent_id: 1 },
        { id: 4, name: 'Laptops', parent_id: 2 },
        { id: 5, name: 'Desktops', parent_id: 2 },
        { id: 6, name: 'Smartphones', parent_id: 3 },
        { id: 7, name: 'Feature Phones', parent_id: 3 }
    ];

    test('TC-020: Block including child when PARENT is EXCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [2]); // Computers excluded

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(4, 'Laptops', 'include', { parentId: 2 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('parent_excluded');
        expect(result.error).toContain('Computers');
    });

    test('TC-021: Block including grandchild when GRANDPARENT is EXCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [1]); // Electronics excluded

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(4, 'Laptops', 'include', { parentId: 2 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('parent_excluded');
        expect(result.error).toContain('Electronics');
    });

    test('TC-022: Allow including child when SIBLING is EXCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [5]); // Desktops (sibling) excluded

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(4, 'Laptops', 'include', { parentId: 2 });

        expect(result.valid).toBe(true);
    });

    test('TC-030: Block including parent when CHILD is EXCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [4]); // Laptops excluded

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(2, 'Computers', 'include', { parentId: 1 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('child_excluded');
        expect(result.error).toContain('Laptops');
    });

    test('TC-031: Block including grandparent when GRANDCHILD is EXCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [4]); // Laptops excluded

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(1, 'Electronics', 'include', { parentId: 0 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('child_excluded');
    });

    test('TC-040: Block excluding child when PARENT is INCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [2]); // Computers included

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(4, 'Laptops', 'exclude', { parentId: 2 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('parent_included');
    });

    test('TC-050: Block excluding parent when CHILDREN are INCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [4, 5]); // Laptops & Desktops included

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(2, 'Computers', 'exclude', { parentId: 1 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('children_included');
    });

    test('TC-051: Block excluding grandparent when GRANDCHILDREN are INCLUDED', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [4]); // Laptops included

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(1, 'Electronics', 'exclude', { parentId: 0 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('children_included');
    });

    test('TC-052: Allow excluding when no children are included', () => {
        const validator = createValidator();
        validator.treeFlatData = treeData;
        const $includePicker = validator.$wrapper.find('.include-picker');
        createMockChips($includePicker, [3]); // Phones (different branch) included

        validator.activeGroup.section = 'exclude';
        const result = validator.validateSelection(2, 'Computers', 'exclude', { parentId: 1 });

        expect(result.valid).toBe(true);
    });
});

// GROUP 4: Non-Tree Entities
describe('Non-Tree Entity Validation', () => {

    test('TC-060: Products - no tree validation, only include/exclude check', () => {
        const validator = createValidator();
        validator.activeGroup.searchEntity = 'products';
        validator.treeFlatData = null;

        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [100]);

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(100, 'Product A', 'include', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('include_exclude_conflict');
    });

    test('TC-061: Manufacturers - allow selection without tree validation', () => {
        const validator = createValidator();
        validator.activeGroup.searchEntity = 'manufacturers';
        validator.treeFlatData = null;

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(50, 'Nike', 'include', {});

        expect(result.valid).toBe(true);
    });
});

// GROUP 5: Edge Cases
describe('Edge Cases', () => {

    test('TC-070: Empty selections - allow any selection', () => {
        const validator = createValidator();
        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(1, 'Category A', 'include', {});

        expect(result.valid).toBe(true);
    });

    test('TC-071: No activeGroup - return valid (fail-safe)', () => {
        const validator = createValidator();
        validator.activeGroup = null;
        const result = validator.validateSelection(1, 'Category A', 'include', {});

        expect(result.valid).toBe(true);
    });

    test('TC-072: String ID coercion to integer', () => {
        const validator = createValidator();
        const $includePicker = validator.$wrapper.find('.include-picker');
        $includePicker.find('.entity-chips').append('<span class="entity-chip" data-id="5"></span>');

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(5, 'Category', 'include', {});

        expect(result.valid).toBe(false);
        expect(result.type).toBe('redundant');
    });

    test('TC-074: Root category (parent_id = 0)', () => {
        const validator = createValidator();
        validator.treeFlatData = [
            { id: 1, name: 'Root', parent_id: 0 },
            { id: 2, name: 'Child', parent_id: 1 }
        ];

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(1, 'Root', 'include', { parentId: 0 });

        expect(result.valid).toBe(true);
    });

    test('TC-075: Deep nesting (6 levels)', () => {
        const validator = createValidator();
        validator.treeFlatData = [
            { id: 1, name: 'Level 1', parent_id: 0 },
            { id: 2, name: 'Level 2', parent_id: 1 },
            { id: 3, name: 'Level 3', parent_id: 2 },
            { id: 4, name: 'Level 4', parent_id: 3 },
            { id: 5, name: 'Level 5', parent_id: 4 },
            { id: 6, name: 'Level 6', parent_id: 5 }
        ];

        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [1]); // Root excluded

        validator.activeGroup.section = 'include';
        const result = validator.validateSelection(6, 'Level 6', 'include', { parentId: 5 });

        expect(result.valid).toBe(false);
        expect(result.type).toBe('parent_excluded');
    });
});

// GROUP 6: Helper Functions
describe('Helper Functions', () => {

    test('TC-080: getChipIds returns correct IDs', () => {
        const validator = createValidator();
        const $picker = validator.$wrapper.find('.include-picker');
        createMockChips($picker, [1, 5, 10, 25]);

        const ids = validator.getChipIds($picker);

        expect(ids).toEqual([1, 5, 10, 25]);
    });

    test('TC-081: getAncestorIds returns correct ancestors', () => {
        const validator = createValidator();
        validator.treeFlatData = [
            { id: 1, name: 'Root', parent_id: 0 },
            { id: 2, name: 'Child', parent_id: 1 },
            { id: 3, name: 'Grandchild', parent_id: 2 }
        ];

        const lookup = {};
        validator.treeFlatData.forEach(item => {
            lookup[item.id] = item;
        });

        const ancestors = validator.getAncestorIds(3, lookup);

        expect(ancestors).toContain(2);
        expect(ancestors).toContain(1);
        expect(ancestors.length).toBe(2);
    });

    test('TC-082: getDescendantIds returns correct descendants', () => {
        const validator = createValidator();
        validator.treeFlatData = [
            { id: 1, name: 'Root', parent_id: 0 },
            { id: 2, name: 'Child A', parent_id: 1 },
            { id: 3, name: 'Child B', parent_id: 1 },
            { id: 4, name: 'Grandchild', parent_id: 2 }
        ];

        const lookup = {};
        validator.treeFlatData.forEach(item => {
            lookup[item.id] = item;
        });

        const descendants = validator.getDescendantIds(1, lookup);

        expect(descendants).toContain(2);
        expect(descendants).toContain(3);
        expect(descendants).toContain(4);
        expect(descendants.length).toBe(3);
    });
});

// GROUP 7: Bulk Validation
describe('Bulk Validation (validatePendingSelections)', () => {

    test('TC-090: Returns errors for conflicts', () => {
        const validator = createValidator();
        const $excludePicker = validator.$wrapper.find('.exclude-picker');
        createMockChips($excludePicker, [5]);

        const pendingSelections = [
            { id: 1, name: 'Category A', data: {} },
            { id: 5, name: 'Category E', data: {} }, // Conflict
            { id: 10, name: 'Category J', data: {} }
        ];

        const errors = validator.validatePendingSelections(pendingSelections, 'include');

        expect(errors.length).toBe(1);
        expect(errors[0].id).toBe(5);
        expect(errors[0].type).toBe('include_exclude_conflict');
    });

    test('TC-091: Returns empty for valid selections', () => {
        const validator = createValidator();

        const pendingSelections = [
            { id: 1, name: 'Category A', data: {} },
            { id: 2, name: 'Category B', data: {} }
        ];

        const errors = validator.validatePendingSelections(pendingSelections, 'include');

        expect(errors.length).toBe(0);
    });

    test('TC-092: Handles empty array', () => {
        const validator = createValidator();
        const errors = validator.validatePendingSelections([], 'include');
        expect(errors.length).toBe(0);
    });

    test('TC-093: Handles null', () => {
        const validator = createValidator();
        const errors = validator.validatePendingSelections(null, 'include');
        expect(errors.length).toBe(0);
    });
});

// =========================================================================
// SUMMARY
// =========================================================================

console.log('\n\x1b[36m══════════════════════════════════════════════════════════════\x1b[0m');
console.log(`\x1b[1mTest Results:\x1b[0m`);
console.log(`  \x1b[32m✓ Passed: ${passed}\x1b[0m`);
if (failed > 0) {
    console.log(`  \x1b[31m✗ Failed: ${failed}\x1b[0m`);
} else {
    console.log('\n  \x1b[32m🎉 All tests passed!\x1b[0m');
}
console.log(`\n  Total: ${passed + failed}`);
console.log('\x1b[36m══════════════════════════════════════════════════════════════\x1b[0m\n');

process.exit(failed > 0 ? 1 : 0);
