/**
 * Unit tests for OrganizationController move-draft-to-current helper functions
 */

import { OrgIdMapping } from '../../interfaces/OrgMapping';

// Mock typeorm
jest.mock('typeorm', () => ({
  Entity: jest.fn(),
  Column: jest.fn(),
  ManyToOne: jest.fn(),
  JoinColumn: jest.fn(),
  OneToMany: jest.fn(),
  In: jest.fn((val: any) => val),
  Like: jest.fn((val: any) => val),
  IsNull: jest.fn(),
  Not: jest.fn(),
}));

// Mock entities
jest.mock('../../entities/OrgRoot', () => ({ OrgRoot: {} }));
jest.mock('../../entities/OrgChild1', () => ({ OrgChild1: {} }));
jest.mock('../../entities/OrgChild2', () => ({ OrgChild2: {} }));
jest.mock('../../entities/OrgChild3', () => ({ OrgChild3: {} }));
jest.mock('../../entities/OrgChild4', () => ({ OrgChild4: {} }));
jest.mock('../../entities/PosMaster', () => ({ PosMaster: {} }));
jest.mock('../../entities/Position', () => ({ Position: {} }));

// Import after mocking
import { In, Like } from 'typeorm';
import { OrgRoot } from '../../entities/OrgRoot';
import { OrgChild1 } from '../../entities/OrgChild1';
import { OrgChild2 } from '../../entities/OrgChild2';
import { OrgChild3 } from '../../entities/OrgChild3';
import { OrgChild4 } from '../../entities/OrgChild4';
import { PosMaster } from '../../entities/PosMaster';
import { Position } from '../../entities/Position';

describe('OrganizationController - Helper Functions', () => {
  let mockQueryRunner: any;
  let mockController: any;

  beforeEach(() => {
    // Mock queryRunner
    mockQueryRunner = {
      manager: {
        find: jest.fn(),
        delete: jest.fn(),
        update: jest.fn(),
        create: jest.fn(),
        save: jest.fn(),
      },
    };

    // Import the controller class (we'll need to mock the private methods)
    // Since we're testing private methods, we'll create a test class
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('resolveOrgId()', () => {
    it('should return null when draftId is null', () => {
      const mapping: OrgIdMapping = {
        byAncestorDNA: new Map(),
        byDraftId: new Map(),
      };

      // Simulate the function logic
      const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => {
        if (!draftId) return null;
        return mapping.byDraftId.get(draftId) ?? null;
      };

      expect(resolveOrgId(null, mapping)).toBeNull();
    });

    it('should return null when draftId is undefined', () => {
      const mapping: OrgIdMapping = {
        byAncestorDNA: new Map(),
        byDraftId: new Map(),
      };

      const resolveOrgId = (draftId: string | null | undefined, mapping: OrgIdMapping): string | null => {
        if (!draftId) return null;
        return mapping.byDraftId.get(draftId) ?? null;
      };

      expect(resolveOrgId(undefined, mapping)).toBeNull();
    });

    it('should return mapped ID when draftId exists in mapping', () => {
      const mapping: OrgIdMapping = {
        byAncestorDNA: new Map(),
        byDraftId: new Map([['draft1', 'current1']]),
      };

      const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => {
        if (!draftId) return null;
        return mapping.byDraftId.get(draftId) ?? null;
      };

      expect(resolveOrgId('draft1', mapping)).toBe('current1');
    });

    it('should return null when draftId does not exist in mapping', () => {
      const mapping: OrgIdMapping = {
        byAncestorDNA: new Map(),
        byDraftId: new Map(),
      };

      const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => {
        if (!draftId) return null;
        return mapping.byDraftId.get(draftId) ?? null;
      };

      expect(resolveOrgId('nonexistent', mapping)).toBeNull();
    });
  });

  describe('cascadeDeletePositions()', () => {
    it('should delete positions with orgRootId when entityClass is OrgRoot', async () => {
      const node = {
        id: 'node1',
        orgRevisionId: 'rev1',
      };

      await mockQueryRunner.manager.delete(PosMaster, {
        orgRevisionId: 'rev1',
        orgRootId: 'node1',
      });

      expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
        orgRevisionId: 'rev1',
        orgRootId: 'node1',
      });
    });

    it('should delete positions with orgChild1Id when entityClass is OrgChild1', async () => {
      const node = {
        id: 'node1',
        orgRevisionId: 'rev1',
      };

      await mockQueryRunner.manager.delete(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild1Id: 'node1',
      });

      expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild1Id: 'node1',
      });
    });

    it('should delete positions with orgChild2Id when entityClass is OrgChild2', async () => {
      const node = {
        id: 'node1',
        orgRevisionId: 'rev1',
      };

      await mockQueryRunner.manager.delete(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild2Id: 'node1',
      });

      expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild2Id: 'node1',
      });
    });

    it('should delete positions with orgChild3Id when entityClass is OrgChild3', async () => {
      const node = {
        id: 'node1',
        orgRevisionId: 'rev1',
      };

      await mockQueryRunner.manager.delete(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild3Id: 'node1',
      });

      expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild3Id: 'node1',
      });
    });

    it('should delete positions with orgChild4Id when entityClass is OrgChild4', async () => {
      const node = {
        id: 'node1',
        orgRevisionId: 'rev1',
      };

      await mockQueryRunner.manager.delete(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild4Id: 'node1',
      });

      expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
        orgRevisionId: 'rev1',
        orgChild4Id: 'node1',
      });
    });
  });

  describe('syncOrgLevel()', () => {
    beforeEach(() => {
      mockQueryRunner.manager.find.mockResolvedValue([]);
      mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 });
      mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 });
      mockQueryRunner.manager.create.mockReturnValue({});
      mockQueryRunner.manager.save.mockResolvedValue({});
    });

    it('should fetch draft and current nodes with Like filter', async () => {
      const repository = {
        find: jest.fn().mockResolvedValue([]),
      };

      await repository.find({
        where: {
          orgRevisionId: 'draftRev1',
          ancestorDNA: Like('root-dna%'),
        },
      });

      await repository.find({
        where: {
          orgRevisionId: 'currentRev1',
          ancestorDNA: Like('root-dna%'),
        },
      });

      expect(repository.find).toHaveBeenCalledTimes(2);
    });

    it('should build lookup maps from draft and current nodes', () => {
      const draftNodes = [
        { id: 'draft1', ancestorDNA: 'root-dna/child1' },
        { id: 'draft2', ancestorDNA: 'root-dna/child2' },
      ];
      const currentNodes = [
        { id: 'current1', ancestorDNA: 'root-dna/child1' },
      ];

      const draftByDNA = new Map(draftNodes.map(n => [n.ancestorDNA, n]));
      const currentByDNA = new Map(currentNodes.map(n => [n.ancestorDNA, n]));

      expect(draftByDNA.size).toBe(2);
      expect(currentByDNA.size).toBe(1);
      expect(draftByDNA.get('root-dna/child1')).toEqual(draftNodes[0]);
      expect(currentByDNA.get('root-dna/child1')).toEqual(currentNodes[0]);
    });

    it('should identify nodes to delete (in current but not in draft)', () => {
      const draftNodes = [
        { id: 'draft1', ancestorDNA: 'root-dna/child1' },
      ];
      const currentNodes = [
        { id: 'current1', ancestorDNA: 'root-dna/child1' },
        { id: 'current2', ancestorDNA: 'root-dna/child2' }, // Not in draft
      ];

      const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n]));
      const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA));

      expect(toDelete).toHaveLength(1);
      expect(toDelete[0].id).toBe('current2');
    });

    it('should identify nodes to update (in both draft and current)', () => {
      const draftNodes = [
        { id: 'draft1', ancestorDNA: 'root-dna/child1' },
        { id: 'draft2', ancestorDNA: 'root-dna/child2' },
      ];
      const currentNodes = [
        { id: 'current1', ancestorDNA: 'root-dna/child1' },
      ];

      const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n]));
      const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA));

      expect(toUpdate).toHaveLength(1);
      expect(toUpdate[0].id).toBe('draft1');
    });

    it('should identify nodes to insert (in draft but not in current)', () => {
      const draftNodes = [
        { id: 'draft1', ancestorDNA: 'root-dna/child1' },
        { id: 'draft2', ancestorDNA: 'root-dna/child2' },
      ];
      const currentNodes = [
        { id: 'current1', ancestorDNA: 'root-dna/child1' },
      ];

      const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n]));
      const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA));

      expect(toInsert).toHaveLength(1);
      expect(toInsert[0].id).toBe('draft2');
    });

    it('should return correct mapping after sync', () => {
      const mapping: OrgIdMapping = {
        byAncestorDNA: new Map([
          ['root-dna/child1', 'current1'],
          ['root-dna/child2', 'current2'],
        ]),
        byDraftId: new Map([
          ['draft1', 'current1'],
          ['draft2', 'current2'],
        ]),
      };

      expect(mapping.byAncestorDNA.get('root-dna/child1')).toBe('current1');
      expect(mapping.byDraftId.get('draft1')).toBe('current1');
      expect(mapping.byDraftId.get('draft2')).toBe('current2');
    });
  });

  describe('syncPositionsForPosMaster()', () => {
    beforeEach(() => {
      mockQueryRunner.manager.find.mockResolvedValue([]);
      mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 });
      mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 });
      mockQueryRunner.manager.create.mockReturnValue({});
      mockQueryRunner.manager.save.mockResolvedValue({});
    });

    it('should fetch draft and current positions for a posMaster', async () => {
      const draftPosMasterId = 'draft-pos-1';
      const currentPosMasterId = 'current-pos-1';

      mockQueryRunner.manager.find
        .mockResolvedValueOnce([{ id: 'pos1', posMasterId: draftPosMasterId }])
        .mockResolvedValueOnce([{ id: 'pos2', posMasterId: currentPosMasterId }]);

      await mockQueryRunner.manager.find(Position, {
        where: { posMasterId: draftPosMasterId },
        order: { orderNo: 'ASC' },
      });

      await mockQueryRunner.manager.find(Position, {
        where: { posMasterId: currentPosMasterId },
      });

      expect(mockQueryRunner.manager.find).toHaveBeenCalledTimes(2);
    });

    it('should delete all current positions when no draft positions exist', async () => {
      const currentPositions = [
        { id: 'pos1', orderNo: 1 },
        { id: 'pos2', orderNo: 2 },
      ];

      mockQueryRunner.manager.find
        .mockResolvedValueOnce([]) // No draft positions
        .mockResolvedValueOnce(currentPositions);

      await mockQueryRunner.manager.delete(Position, ['pos1', 'pos2']);

      expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(Position, ['pos1', 'pos2']);
    });

    it('should delete positions not in draft (by orderNo)', async () => {
      const draftPositions = [
        { id: 'dpos1', orderNo: 1 },
        { id: 'dpos2', orderNo: 2 },
      ];
      const currentPositions = [
        { id: 'cpos1', orderNo: 1 },
        { id: 'cpos2', orderNo: 2 },
        { id: 'cpos3', orderNo: 3 }, // Not in draft
      ];

      mockQueryRunner.manager.find
        .mockResolvedValueOnce(draftPositions)
        .mockResolvedValueOnce(currentPositions);

      const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo));
      const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo));

      expect(toDelete).toHaveLength(1);
      expect(toDelete[0].id).toBe('cpos3');
    });

    it('should update existing positions (matched by orderNo)', async () => {
      const draftPositions = [
        {
          id: 'dpos1',
          orderNo: 1,
          positionName: 'Updated Name',
          positionField: 'field1',
          posTypeId: 'type1',
          posLevelId: 'level1',
        },
      ];
      const currentPositions = [
        { id: 'cpos1', orderNo: 1, positionName: 'Old Name' },
      ];

      const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p]));
      const draftPos = draftPositions[0];
      const current = currentByOrderNo.get(draftPos.orderNo);

      expect(current).toBeDefined();

      if (current) {
        const updateData = {
          positionName: draftPos.positionName,
          positionField: draftPos.positionField,
          posTypeId: draftPos.posTypeId,
          posLevelId: draftPos.posLevelId,
        };

        await mockQueryRunner.manager.update(Position, current.id, updateData);

        expect(mockQueryRunner.manager.update).toHaveBeenCalledWith(
          Position,
          'cpos1',
          expect.objectContaining({ positionName: 'Updated Name' })
        );
      }
    });

    it('should insert new positions not in current', async () => {
      const draftPositions = [
        { id: 'dpos1', orderNo: 1, positionName: 'New Position' },
      ];
      const currentPositions: any[] = [];

      const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p]));
      const draftPos = draftPositions[0];
      const current = currentByOrderNo.get(draftPos.orderNo);
      const currentPosMasterId = 'current-pos-1';

      expect(current).toBeUndefined();

      if (!current) {
        const newPosition = {
          ...draftPos,
          id: undefined,
          posMasterId: currentPosMasterId,
        };

        await mockQueryRunner.manager.create(Position, newPosition);
        await mockQueryRunner.manager.save(newPosition);

        expect(mockQueryRunner.manager.create).toHaveBeenCalledWith(Position, {
          ...draftPos,
          id: undefined,
          posMasterId: currentPosMasterId,
        });
      }
    });
  });
});
