import { DataSource } from 'typeorm';
import { ProjectTemplateService } from '../../../src/services/project/project-template.service';
import { ProjectTemplateEntity, ProjectTemplateSchema } from '../../../src/entities/project-template.entity';
import { ProjectEntity } from '../../../src/entities/project.entity';

describe('ProjectTemplateService', () => {
  let dataSource: DataSource;
  let templateService: ProjectTemplateService;
  let mockTemplateRepo: any;
  let mockProjectRepo: any;

  beforeEach(() => {
    // Mock repository methods
    mockTemplateRepo = {
      create: jest.fn(),
      save: jest.fn(),
      findOne: jest.fn(),
      find: jest.fn(),
      delete: jest.fn(),
      count: jest.fn(),
      update: jest.fn(),
      createQueryBuilder: jest.fn(() => ({
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        select: jest.fn().mockReturnThis(),
        addSelect: jest.fn().mockReturnThis(),
        groupBy: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        addOrderBy: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        getRawMany: jest.fn().mockResolvedValue([]),
        getMany: jest.fn().mockResolvedValue([]),
        getOne: jest.fn().mockResolvedValue(null)
      }))
    };

    mockProjectRepo = {
      create: jest.fn(),
      save: jest.fn(),
      findOne: jest.fn(),
      find: jest.fn()
    };

    // Mock DataSource
    dataSource = {
      getRepository: jest.fn((entity) => {
        if (entity === ProjectTemplateEntity) {
          return mockTemplateRepo;
        }
        if (entity === ProjectEntity) {
          return mockProjectRepo;
        }
        return {};
      }),
      transaction: jest.fn(async (callback) => {
        const mockManager = {
          getRepository: jest.fn((entity) => {
            if (entity === ProjectTemplateEntity) {
              return mockTemplateRepo;
            }
            if (entity === ProjectEntity) {
              return mockProjectRepo;
            }
            return {};
          })
        };
        return await callback(mockManager);
      })
    } as unknown as DataSource;

    templateService = new ProjectTemplateService(dataSource);
  });

  describe('createTemplate', () => {
    it('should create a new template successfully', async () => {
      const templateSchema: ProjectTemplateSchema = {
        name: 'Test Project',
        description: 'Test Description',
        status: 'active',
        priority: 'high',
        durationDays: 30,
        tasks: [
          {
            title: 'Task 1',
            description: 'Task 1 description',
            priority: 'high'
          }
        ]
      };

      const createDto = {
        name: 'Test Template',
        description: 'A test template',
        category: 'project' as const,
        schema: templateSchema,
        tags: ['test', 'demo']
      };

      const createdTemplate = {
        id: 'template-1',
        ...createDto,
        status: 'draft',
        usageStats: { useCount: 0 }
      };

      mockTemplateRepo.findOne.mockResolvedValue(null);
      mockTemplateRepo.create.mockReturnValue(createdTemplate);
      mockTemplateRepo.save.mockResolvedValue(createdTemplate);

      const result = await templateService.createTemplate(createDto);

      expect(result).toEqual(createdTemplate);
      expect(mockTemplateRepo.findOne).toHaveBeenCalledWith({
        where: { name: 'Test Template' }
      });
      expect(mockTemplateRepo.create).toHaveBeenCalledWith(expect.objectContaining({
        name: 'Test Template',
        schema: templateSchema
      }));
    });

    it('should throw error if template name already exists', async () => {
      mockTemplateRepo.findOne.mockResolvedValue({ id: 'existing', name: 'Test Template' });

      await expect(
        templateService.createTemplate({
          name: 'Test Template',
          schema: { name: 'Test' },
          category: 'project'
        })
      ).rejects.toThrow('Template with name "Test Template" already exists');
    });

    it('should validate parent template if specified', async () => {
      const createDto = {
        name: 'Child Template',
        schema: { name: 'Child' },
        parentTemplateId: 'parent-1',
        category: 'project' as const
      };

      mockTemplateRepo.findOne
        .mockResolvedValueOnce(null) // Name check
        .mockResolvedValueOnce(null); // Parent check

      await expect(templateService.createTemplate(createDto))
        .rejects.toThrow('Parent template with ID parent-1 not found');
    });
  });

  describe('updateTemplate', () => {
    it('should update template successfully', async () => {
      const existingTemplate = {
        id: 'template-1',
        name: 'Original Template',
        description: 'Original description',
        schema: { name: 'Original' },
        category: 'project'
      };

      const updateDto = {
        id: 'template-1',
        description: 'Updated description',
        schema: { name: 'Updated' }
      };

      mockTemplateRepo.findOne.mockResolvedValue(existingTemplate);
      mockTemplateRepo.save.mockResolvedValue({
        ...existingTemplate,
        ...updateDto
      });

      const result = await templateService.updateTemplate(updateDto);

      expect(result.description).toBe('Updated description');
      expect(mockTemplateRepo.save).toHaveBeenCalled();
    });

    it('should throw error if template not found', async () => {
      mockTemplateRepo.findOne.mockResolvedValue(null);

      await expect(
        templateService.updateTemplate({
          id: 'non-existent',
          description: 'Updated'
        })
      ).rejects.toThrow('Template with ID non-existent not found');
    });

    it('should check name uniqueness on update', async () => {
      const existingTemplate = {
        id: 'template-1',
        name: 'Original',
        schema: { name: 'Original' }
      };

      mockTemplateRepo.findOne
        .mockResolvedValueOnce(existingTemplate) // Find template to update
        .mockResolvedValueOnce({ id: 'template-2', name: 'NewName' }); // Name already exists

      await expect(
        templateService.updateTemplate({
          id: 'template-1',
          name: 'NewName'
        })
      ).rejects.toThrow('Template with name "NewName" already exists');
    });
  });

  describe('deleteTemplate', () => {
    it('should delete template successfully', async () => {
      mockTemplateRepo.findOne.mockResolvedValue({
        id: 'template-1',
        name: 'Test Template'
      });
      mockTemplateRepo.count.mockResolvedValue(0);
      mockTemplateRepo.delete.mockResolvedValue({ affected: 1 });

      const result = await templateService.deleteTemplate('template-1');

      expect(result).toBe(true);
      expect(mockTemplateRepo.delete).toHaveBeenCalledWith('template-1');
    });

    it('should prevent deletion if template has children', async () => {
      mockTemplateRepo.findOne.mockResolvedValue({
        id: 'template-1',
        name: 'Parent Template'
      });
      mockTemplateRepo.count.mockResolvedValue(2); // Has child templates

      await expect(templateService.deleteTemplate('template-1'))
        .rejects.toThrow('Cannot delete template that is being used as a parent by other templates');
    });
  });

  describe('applyTemplate', () => {
    it('should apply template to create project successfully', async () => {
      const template = {
        id: 'template-1',
        name: 'Test Template',
        status: 'published',
        schema: {
          name: 'Template Project',
          description: 'From template',
          status: 'active',
          priority: 'high',
          durationDays: 30,
          structure: {
            children: [
              {
                name: 'Phase 1',
                durationDays: 10
              },
              {
                name: 'Phase 2',
                durationDays: 20
              }
            ]
          }
        } as ProjectTemplateSchema,
        usageStats: { useCount: 0 }
      };

      const createdProject = {
        id: 'project-1',
        name: 'New Project',
        description: 'From template',
        path: '0001'
      };

      // Mock template retrieval
      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(template as ProjectTemplateEntity);

      // Mock ProjectTreeService.insertNode
      const projectTreeService = (templateService as any).projectTreeService;
      jest.spyOn(projectTreeService, 'insertNode').mockResolvedValue(createdProject);

      // Mock template repository update for usage stats
      mockTemplateRepo.update.mockResolvedValue({ affected: 1 });

      const result = await templateService.applyTemplate('template-1', {
        projectName: 'New Project',
        startDate: new Date('2025-01-01')
      });

      expect(result.success).toBe(true);
      expect(result.projectId).toBe('project-1');
      expect(result.createdProjects).toHaveLength(3); // 1 root + 2 children
    });

    it('should handle deprecated templates', async () => {
      const template = {
        id: 'template-1',
        status: 'deprecated',
        schema: { name: 'Deprecated' }
      };

      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(template as ProjectTemplateEntity);

      const result = await templateService.applyTemplate('template-1', {
        projectName: 'New Project'
      });

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Cannot apply a deprecated template');
    });

    it('should validate variables if validation schema exists', async () => {
      const template = {
        id: 'template-1',
        status: 'published',
        schema: { name: 'Template' },
        validationSchema: {
          type: 'object',
          properties: {
            projectType: { type: 'string', enum: ['web', 'mobile'] }
          },
          required: ['projectType']
        },
        usageStats: { useCount: 0 }
      };

      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(template as ProjectTemplateEntity);

      const result = await templateService.applyTemplate('template-1', {
        projectName: 'New Project',
        variables: { invalidField: 'value' } // Missing required field
      });

      expect(result.success).toBe(false);
      expect(result.errors).toBeDefined();
    });
  });

  describe('validateTemplate', () => {
    it('should validate template with invalid task dependencies', async () => {
      const template = {
        id: 'template-1',
        schema: {
          name: 'Valid Template',
          durationDays: 30,
          tasks: [
            { title: 'Task 1', dependencies: ['999'] }, // Non-existent dependency
            { title: 'Task 2' }
          ],
          customFields: [
            {
              name: 'projectType',
              type: 'select',
              label: 'Project Type',
              options: [
                { value: 'web', label: 'Web' },
                { value: 'mobile', label: 'Mobile' }
              ]
            }
          ]
        } as ProjectTemplateSchema
      };

      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(template as ProjectTemplateEntity);

      const result = await templateService.validateTemplate('template-1');

      expect(result.valid).toBe(false); 
      expect(result.errors).toContain('Task 0 has invalid dependency: 999');
    });

    it('should detect invalid regex patterns in custom fields', async () => {
      const template = {
        id: 'template-1',
        schema: {
          name: 'Template',
          customFields: [
            {
              name: 'code',
              type: 'text',
              label: 'Code',
              validation: {
                pattern: '[invalid regex'
              }
            }
          ]
        } as ProjectTemplateSchema
      };

      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(template as ProjectTemplateEntity);

      const result = await templateService.validateTemplate('template-1');

      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Custom field "code" has invalid regex pattern');
    });
  });

  describe('searchTemplates', () => {
    it('should search templates with criteria', async () => {
      const templates = [
        {
          id: 'template-1',
          name: 'Web Template',
          category: 'project',
          status: 'published',
          tags: ['web', 'frontend']
        },
        {
          id: 'template-2',
          name: 'Mobile Template',
          category: 'project',
          status: 'published',
          tags: ['mobile', 'ios']
        }
      ];

      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        addOrderBy: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue(templates)
      };

      mockTemplateRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await templateService.searchTemplates({
        category: 'project',
        status: 'published',
        searchTerm: 'Template'
      });

      expect(result).toEqual(templates);
      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'template.category = :category',
        { category: 'project' }
      );
    });
  });

  describe('cloneTemplate', () => {
    it('should clone template successfully', async () => {
      const sourceTemplate = {
        id: 'template-1',
        name: 'Original',
        description: 'Original template',
        category: 'project',
        schema: { name: 'Original Schema' },
        tags: ['original']
      };

      const clonedTemplate = {
        id: 'template-2',
        name: 'Cloned Template',
        description: 'Cloned from Original',
        category: 'project',
        schema: { name: 'Original Schema' },
        tags: ['original', 'cloned']
      };

      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(sourceTemplate as ProjectTemplateEntity);
      jest.spyOn(templateService, 'createTemplate').mockResolvedValue(clonedTemplate as ProjectTemplateEntity);

      const result = await templateService.cloneTemplate('template-1', 'Cloned Template');

      expect(result).toEqual(clonedTemplate);
      expect(templateService.createTemplate).toHaveBeenCalledWith(
        expect.objectContaining({
          name: 'Cloned Template',
          tags: ['original', 'cloned']
        })
      );
    });
  });

  describe('getTemplateStatistics', () => {
    it('should return template statistics', async () => {
      mockTemplateRepo.count.mockResolvedValue(10);
      
      const mockQueryBuilder = {
        select: jest.fn().mockReturnThis(),
        addSelect: jest.fn().mockReturnThis(),
        groupBy: jest.fn().mockReturnThis(),
        where: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        limit: jest.fn().mockReturnThis(),
        getRawMany: jest.fn()
          .mockResolvedValueOnce([
            { category: 'project', count: '5' },
            { category: 'workflow', count: '3' }
          ])
          .mockResolvedValueOnce([
            { status: 'published', count: '6' },
            { status: 'draft', count: '4' }
          ]),
        getMany: jest.fn()
          .mockResolvedValueOnce([]) // Most used
          .mockResolvedValueOnce([]) // Recently created
      };

      mockTemplateRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder);

      const result = await templateService.getTemplateStatistics();

      expect(result.totalTemplates).toBe(10);
      expect(result.byCategory).toEqual({
        project: 5,
        workflow: 3
      });
      expect(result.byStatus).toEqual({
        published: 6,
        draft: 4
      });
    });
  });

  describe('Template Schema Validation', () => {
    it('should validate hierarchical structure', async () => {
      const schema: ProjectTemplateSchema = {
        name: 'Root',
        structure: {
          children: [
            {
              name: 'Child 1',
              children: [
                { name: 'Grandchild 1' },
                { name: 'Grandchild 2' }
              ]
            },
            {
              name: 'Child 2',
              durationDays: -5 // Invalid negative duration
            }
          ]
        }
      };

      const createDto = {
        name: 'Invalid Template',
        schema,
        category: 'project' as const
      };

      mockTemplateRepo.findOne.mockResolvedValue(null);

      await expect(templateService.createTemplate(createDto))
        .rejects.toThrow('Template schema validation failed');
    });

    it('should validate dependencies', async () => {
      const schema: ProjectTemplateSchema = {
        name: 'Template',
        dependencies: [
          {
            from: 'project1',
            to: 'project1', // Self-reference
            type: 'blocks'
          }
        ]
      };

      const createDto = {
        name: 'Dependency Template',
        schema,
        category: 'project' as const
      };

      mockTemplateRepo.findOne.mockResolvedValue(null);

      await expect(templateService.createTemplate(createDto))
        .rejects.toThrow('Template schema validation failed: Dependency cannot reference itself');
    });
  });

  describe('Variable Substitution', () => {
    it('should substitute variables in template', async () => {
      const template = {
        id: 'template-1',
        status: 'published',
        schema: {
          name: 'Project {{projectName}}',
          description: 'A {{projectType}} project for {{client}}',
          metadata: {
            owner: '{{owner}}',
            budget: '{{budget}}'
          }
        } as ProjectTemplateSchema,
        usageStats: { useCount: 0 }
      };

      const variables = {
        projectName: 'Alpha',
        projectType: 'Web',
        client: 'ACME Corp',
        owner: 'John Doe',
        budget: '50000'
      };

      jest.spyOn(templateService, 'getTemplate').mockResolvedValue(template as ProjectTemplateEntity);

      // Mock project creation
      const createdProject = {
        id: 'project-1',
        name: 'Project Alpha',
        description: 'A Web project for ACME Corp',
        path: '0001',
        metadata: {
          owner: 'John Doe',
          budget: '50000',
          templateId: 'template-1',
          templateName: 'Test Template',
          appliedAt: new Date()
        }
      };

      // Mock ProjectTreeService.insertNode
      const projectTreeService = (templateService as any).projectTreeService;
      jest.spyOn(projectTreeService, 'insertNode').mockResolvedValue(createdProject);

      // Mock template repository update for usage stats
      mockTemplateRepo.update.mockResolvedValue({ affected: 1 });

      const result = await templateService.applyTemplate('template-1', {
        projectName: 'Project Alpha',
        variables
      });

      expect(result.success).toBe(true);
      expect(projectTreeService.insertNode).toHaveBeenCalledWith(
        expect.objectContaining({
          name: 'Project Alpha',
          description: 'A Web project for ACME Corp'
        }),
        undefined
      );
    });
  });
});