import { describe, expect, it } from 'vitest';
import { BuilderFurniPlaceableStatus } from '../../api/catalog/BuilderFurniPlaceableStatus';
import { CatalogType } from '../../api/catalog/CatalogType';
import {
    buildCatalogNodeTree,
    findNodeById,
    findNodeByName,
    getNodesByOfferIdFromMap,
    getOfferProductKeys,
    normalizeCatalogType,
    resolveBuilderFurniPlaceableStatus
} from './useCatalog.helpers';

// ---------------------------------------------------------------------------
// normalizeCatalogType
// ---------------------------------------------------------------------------

describe('normalizeCatalogType', () =>
{
    it('returns BUILDER when explicitly asked for BUILDER', () =>
    {
        expect(normalizeCatalogType(CatalogType.BUILDER)).toBe(CatalogType.BUILDER);
    });

    it('returns NORMAL for the explicit NORMAL value', () =>
    {
        expect(normalizeCatalogType(CatalogType.NORMAL)).toBe(CatalogType.NORMAL);
    });

    it('returns NORMAL when type is omitted', () =>
    {
        expect(normalizeCatalogType()).toBe(CatalogType.NORMAL);
    });

    it('returns NORMAL for any unknown string', () =>
    {
        expect(normalizeCatalogType('something_else')).toBe(CatalogType.NORMAL);
        expect(normalizeCatalogType('')).toBe(CatalogType.NORMAL);
    });
});

// ---------------------------------------------------------------------------
// getOfferProductKeys
// ---------------------------------------------------------------------------

describe('getOfferProductKeys', () =>
{
    const makeOffer = (overrides: any = {}) =>
        ({
            product: {
                productType: 'floor',
                productClassId: 42,
                furnitureData: { className: 'chair_basic' },
                ...overrides
            }
        }) as any;

    it('returns both id and className keys when the product has both', () =>
    {
        expect(getOfferProductKeys(makeOffer())).toEqual([
            'floor:id:42',
            'floor:class:chair_basic'
        ]);
    });

    it('omits the id key when productClassId is negative', () =>
    {
        const offer = makeOffer({ productClassId: -1 });

        expect(getOfferProductKeys(offer)).toEqual([ 'floor:class:chair_basic' ]);
    });

    it('omits the className key when furnitureData has no className', () =>
    {
        const offer = makeOffer({ furnitureData: { className: '' } });

        expect(getOfferProductKeys(offer)).toEqual([ 'floor:id:42' ]);
    });

    it('returns an empty array when the offer has no product', () =>
    {
        expect(getOfferProductKeys(null)).toEqual([]);
        expect(getOfferProductKeys(undefined)).toEqual([]);
        expect(getOfferProductKeys({} as any)).toEqual([]);
    });

    it('returns an empty array when productType is missing', () =>
    {
        const offer = makeOffer({ productType: '' });

        expect(getOfferProductKeys(offer)).toEqual([]);
    });
});

// ---------------------------------------------------------------------------
// findNodeById / findNodeByName
// ---------------------------------------------------------------------------

const makeNode = (overrides: { pageId?: number; pageName?: string; children?: any[] } = {}) =>
    ({
        pageId: overrides.pageId ?? -1,
        pageName: overrides.pageName ?? 'unnamed',
        isVisible: true,
        children: overrides.children ?? []
    });

describe('findNodeById', () =>
{
    it('returns null when the input node is null', () =>
    {
        expect(findNodeById(7, null, null)).toBeNull();
    });

    it('skips the root node even when its pageId matches', () =>
    {
        const root = makeNode({ pageId: 7, pageName: 'root' }) as any;

        expect(findNodeById(7, root, root)).toBeNull();
    });

    it('finds an immediate child by pageId', () =>
    {
        const child = makeNode({ pageId: 7, pageName: 'shop' }) as any;
        const root = makeNode({ pageId: 0, pageName: 'root', children: [ child ] }) as any;

        expect(findNodeById(7, root, root)).toBe(child);
    });

    it('descends into grandchildren', () =>
    {
        const grandchild = makeNode({ pageId: 42, pageName: 'sale' }) as any;
        const child = makeNode({ pageId: 7, pageName: 'shop', children: [ grandchild ] }) as any;
        const root = makeNode({ pageId: 0, pageName: 'root', children: [ child ] }) as any;

        expect(findNodeById(42, root, root)).toBe(grandchild);
    });

    it('returns null when no node has that pageId', () =>
    {
        const child = makeNode({ pageId: 7, pageName: 'shop' }) as any;
        const root = makeNode({ pageId: 0, pageName: 'root', children: [ child ] }) as any;

        expect(findNodeById(99, root, root)).toBeNull();
    });
});

describe('findNodeByName', () =>
{
    it('finds a node by pageName ignoring the root', () =>
    {
        const child = makeNode({ pageName: 'frontpage' }) as any;
        const root = makeNode({ pageName: 'root', children: [ child ] }) as any;

        expect(findNodeByName('frontpage', root, root)).toBe(child);
        expect(findNodeByName('root', root, root)).toBeNull();
    });

    it('returns null when nothing matches', () =>
    {
        const root = makeNode({ pageName: 'root', children: [ makeNode({ pageName: 'a' }) as any ] }) as any;

        expect(findNodeByName('b', root, root)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// getNodesByOfferIdFromMap
// ---------------------------------------------------------------------------

describe('getNodesByOfferIdFromMap', () =>
{
    const visibleNode = (id: number) => ({ pageId: id, isVisible: true } as any);
    const hiddenNode = (id: number) => ({ pageId: id, isVisible: false } as any);

    it('returns null when the map is missing or empty', () =>
    {
        expect(getNodesByOfferIdFromMap(1, null)).toBeNull();
        expect(getNodesByOfferIdFromMap(1, undefined)).toBeNull();
        expect(getNodesByOfferIdFromMap(1, new Map())).toBeNull();
    });

    it('returns the raw bucket when onlyVisible is false', () =>
    {
        const bucket = [ visibleNode(1), hiddenNode(2) ];
        const map = new Map([ [ 9, bucket ] ]);

        expect(getNodesByOfferIdFromMap(9, map)).toBe(bucket);
    });

    it('filters out hidden nodes when onlyVisible is true', () =>
    {
        const visible = visibleNode(1);
        const map = new Map([ [ 9, [ visible, hiddenNode(2) ] ] ]);

        expect(getNodesByOfferIdFromMap(9, map, true)).toEqual([ visible ]);
    });

    it('falls back to the raw bucket when no visible nodes remain', () =>
    {
        const bucket = [ hiddenNode(1), hiddenNode(2) ];
        const map = new Map([ [ 9, bucket ] ]);

        expect(getNodesByOfferIdFromMap(9, map, true)).toBe(bucket);
    });

    it('returns null for an offerId not in the map', () =>
    {
        const map = new Map([ [ 1, [ visibleNode(1) ] ] ]);

        expect(getNodesByOfferIdFromMap(99, map)).toBeNull();
    });
});

// ---------------------------------------------------------------------------
// buildCatalogNodeTree
// ---------------------------------------------------------------------------

describe('buildCatalogNodeTree', () =>
{
    // CatalogNode in the real codebase reads `node.pageId`, `node.pageName`,
    // `node.offerIds`, `node.children`. Anything else is irrelevant to the
    // build step.
    const makeNodeData = (overrides: any = {}) =>
        ({
            pageId: -1,
            pageName: 'unnamed',
            localization: '',
            iconId: -1,
            offerIds: [] as number[],
            children: [] as any[],
            visible: true,
            ...overrides
        });

    it('returns a CatalogNode root with the depth=0', () =>
    {
        const rootData = makeNodeData({ pageId: 0, pageName: 'root' });
        const { rootNode, offersToNodes } = buildCatalogNodeTree(rootData as any);

        expect(rootNode.pageId).toBe(0);
        expect(rootNode.depth).toBe(0);
        expect(offersToNodes.size).toBe(0);
    });

    it('walks children depth-first and tracks offerId mappings', () =>
    {
        const leaf = makeNodeData({ pageId: 5, pageName: 'sale', offerIds: [ 100, 200 ] });
        const branch = makeNodeData({ pageId: 3, pageName: 'shop', offerIds: [ 100 ], children: [ leaf ] });
        const rootData = makeNodeData({ pageId: 0, pageName: 'root', children: [ branch ] });

        const { rootNode, offersToNodes } = buildCatalogNodeTree(rootData as any);

        // tree shape
        expect(rootNode.children).toHaveLength(1);
        expect(rootNode.children[0].pageId).toBe(3);
        expect(rootNode.children[0].children[0].pageId).toBe(5);

        // depth incremented
        expect(rootNode.depth).toBe(0);
        expect(rootNode.children[0].depth).toBe(1);
        expect(rootNode.children[0].children[0].depth).toBe(2);

        // offerId index records both nodes for offer 100, only the leaf for 200
        expect(offersToNodes.get(100)).toHaveLength(2);
        expect(offersToNodes.get(100)?.map(n => n.pageId)).toEqual([ 3, 5 ]);
        expect(offersToNodes.get(200)?.map(n => n.pageId)).toEqual([ 5 ]);
    });

    it('preserves child-parent relationships', () =>
    {
        const leaf = makeNodeData({ pageId: 5, pageName: 'sale' });
        const rootData = makeNodeData({ pageId: 0, pageName: 'root', children: [ leaf ] });

        const { rootNode } = buildCatalogNodeTree(rootData as any);

        expect(rootNode.children[0].parent).toBe(rootNode);
    });
});

// ---------------------------------------------------------------------------
// resolveBuilderFurniPlaceableStatus
// ---------------------------------------------------------------------------

describe('resolveBuilderFurniPlaceableStatus', () =>
{
    const offer = { offerId: 1 } as any;

    const baseInput = {
        offer,
        roomSession: { isGuildRoom: false, isRoomOwner: true, controllerLevel: 0 },
        secondsLeft: 60,
        furniCount: 0,
        furniLimit: 10,
        builderPlacementAllowedInCurrentRoom: false,
        builderPlacementBlockedByVisitors: false,
        visitorCount: 0
    };

    it('returns MISSING_OFFER when offer is null', () =>
    {
        expect(resolveBuilderFurniPlaceableStatus({ ...baseInput, offer: null })).toBe(BuilderFurniPlaceableStatus.MISSING_OFFER);
    });

    it('returns NOT_IN_ROOM when roomSession is null', () =>
    {
        expect(resolveBuilderFurniPlaceableStatus({ ...baseInput, roomSession: null })).toBe(BuilderFurniPlaceableStatus.NOT_IN_ROOM);
    });

    it('returns OKAY for the room owner with time on the clock', () =>
    {
        expect(resolveBuilderFurniPlaceableStatus(baseInput)).toBe(BuilderFurniPlaceableStatus.OKAY);
    });

    it('returns NOT_GROUP_ADMIN for a non-owner without group fallback or shared pool', () =>
    {
        const input = {
            ...baseInput,
            roomSession: { isGuildRoom: false, isRoomOwner: false, controllerLevel: 0 }
        };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN);
    });

    it('returns OKAY for guild admin with subscription time remaining', () =>
    {
        const input = {
            ...baseInput,
            roomSession: { isGuildRoom: true, isRoomOwner: false, controllerLevel: 4 /* GUILD_ADMIN */ },
            secondsLeft: 60
        };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.OKAY);
    });

    it('returns FURNI_LIMIT_REACHED when count meets the limit and no shared pool applies', () =>
    {
        const input = { ...baseInput, furniCount: 10, furniLimit: 10 };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED);
    });

    it('skips the furni limit when builderPlacementAllowedInCurrentRoom for a non-owner', () =>
    {
        const input = {
            ...baseInput,
            roomSession: { isGuildRoom: false, isRoomOwner: false, controllerLevel: 0 },
            furniCount: 99,
            furniLimit: 10,
            builderPlacementAllowedInCurrentRoom: true
        };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.OKAY);
    });

    it('returns VISITORS_IN_ROOM when the subscription has expired and the flag is set', () =>
    {
        const input = {
            ...baseInput,
            secondsLeft: 0,
            builderPlacementBlockedByVisitors: true
        };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.VISITORS_IN_ROOM);
    });

    it('returns VISITORS_IN_ROOM when the subscription has expired and there are visitors counted', () =>
    {
        const input = { ...baseInput, secondsLeft: 0, visitorCount: 3 };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.VISITORS_IN_ROOM);
    });

    it('returns OKAY when the subscription has expired but the room is empty', () =>
    {
        const input = { ...baseInput, secondsLeft: 0, visitorCount: 0 };

        expect(resolveBuilderFurniPlaceableStatus(input)).toBe(BuilderFurniPlaceableStatus.OKAY);
    });
});
