/**
 * DrizzlePhaseRepository Tests
 *
 * Tests for the Phase repository adapter.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzlePhaseRepository } from './phase.js';
import { DrizzleInitiativeRepository } from './initiative.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';

describe('DrizzlePhaseRepository', () => {
  let db: DrizzleDatabase;
  let phaseRepo: DrizzlePhaseRepository;
  let initiativeRepo: DrizzleInitiativeRepository;
  let testInitiativeId: string;

  beforeEach(async () => {
    db = createTestDatabase();
    phaseRepo = new DrizzlePhaseRepository(db);
    initiativeRepo = new DrizzleInitiativeRepository(db);

    // Create a test initiative for FK constraint
    const initiative = await initiativeRepo.create({
      name: 'Test Initiative',
    });
    testInitiativeId = initiative.id;
  });

  describe('create', () => {
    it('should create a phase with generated id and timestamps', async () => {
      const phase = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });

      expect(phase.id).toBeDefined();
      expect(phase.id.length).toBeGreaterThan(0);
      expect(phase.initiativeId).toBe(testInitiativeId);
      expect(phase.name).toBe('Test Phase');
      expect(phase.status).toBe('pending');
      expect(phase.createdAt).toBeInstanceOf(Date);
      expect(phase.updatedAt).toBeInstanceOf(Date);
    });

    it('should throw for invalid initiativeId (FK constraint)', async () => {
      await expect(
        phaseRepo.create({
          initiativeId: 'invalid-initiative-id',
          name: 'Invalid Phase',
        })
      ).rejects.toThrow();
    });
  });

  describe('findById', () => {
    it('should return null for non-existent phase', async () => {
      const result = await phaseRepo.findById('non-existent-id');
      expect(result).toBeNull();
    });

    it('should find an existing phase', async () => {
      const created = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Find Me',
      });

      const found = await phaseRepo.findById(created.id);
      expect(found).not.toBeNull();
      expect(found!.id).toBe(created.id);
      expect(found!.name).toBe('Find Me');
    });
  });

  describe('findByInitiativeId', () => {
    it('should return empty array for initiative with no phases', async () => {
      const phases = await phaseRepo.findByInitiativeId(testInitiativeId);
      expect(phases).toEqual([]);
    });

    it('should return only matching phases ordered by createdAt', async () => {
      await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase A',
      });
      await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase B',
      });
      await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase C',
      });

      // Create another initiative with phases
      const otherInitiative = await initiativeRepo.create({
        name: 'Other Initiative',
      });
      await phaseRepo.create({
        initiativeId: otherInitiative.id,
        name: 'Other Phase',
      });

      const phases = await phaseRepo.findByInitiativeId(testInitiativeId);
      expect(phases.length).toBe(3);
      expect(phases[0].name).toBe('Phase A');
      expect(phases[1].name).toBe('Phase B');
      expect(phases[2].name).toBe('Phase C');
    });
  });

  describe('update', () => {
    it('should update fields and updatedAt', async () => {
      const created = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Original Name',
        status: 'pending',
      });

      await new Promise((resolve) => setTimeout(resolve, 10));

      const updated = await phaseRepo.update(created.id, {
        name: 'Updated Name',
        status: 'in_progress',
      });

      expect(updated.name).toBe('Updated Name');
      expect(updated.status).toBe('in_progress');
      expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(created.updatedAt.getTime());
    });

    it('should throw for non-existent phase', async () => {
      await expect(
        phaseRepo.update('non-existent-id', { name: 'New Name' })
      ).rejects.toThrow('Phase not found');
    });
  });

  describe('delete', () => {
    it('should delete an existing phase', async () => {
      const created = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'To Delete',
      });

      await phaseRepo.delete(created.id);

      const found = await phaseRepo.findById(created.id);
      expect(found).toBeNull();
    });

    it('should throw for non-existent phase', async () => {
      await expect(phaseRepo.delete('non-existent-id')).rejects.toThrow(
        'Phase not found'
      );
    });
  });

  // ===========================================================================
  // Phase Dependency Tests
  // ===========================================================================

  describe('createDependency', () => {
    it('should create dependency between two phases', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });

      await phaseRepo.createDependency(phase2.id, phase1.id);

      const deps = await phaseRepo.getDependencies(phase2.id);
      expect(deps).toContain(phase1.id);
    });

    it('should throw if phase does not exist', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });

      // Creating dependency with non-existent phase should throw (FK constraint)
      await expect(
        phaseRepo.createDependency('non-existent-phase', phase1.id)
      ).rejects.toThrow();
    });

    it('should allow multiple dependencies for same phase', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      const phase3 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 3',
      });

      // Phase 3 depends on both Phase 1 and Phase 2
      await phaseRepo.createDependency(phase3.id, phase1.id);
      await phaseRepo.createDependency(phase3.id, phase2.id);

      const deps = await phaseRepo.getDependencies(phase3.id);
      expect(deps.length).toBe(2);
      expect(deps).toContain(phase1.id);
      expect(deps).toContain(phase2.id);
    });
  });

  describe('getDependencies', () => {
    it('should return empty array for phase with no dependencies', async () => {
      const phase = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });

      const deps = await phaseRepo.getDependencies(phase.id);
      expect(deps).toEqual([]);
    });

    it('should return dependency IDs for phase with dependencies', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      const phase3 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 3',
      });

      // Phase 3 depends on Phase 1 and Phase 2
      await phaseRepo.createDependency(phase3.id, phase1.id);
      await phaseRepo.createDependency(phase3.id, phase2.id);

      const deps = await phaseRepo.getDependencies(phase3.id);
      expect(deps.length).toBe(2);
      expect(deps).toContain(phase1.id);
      expect(deps).toContain(phase2.id);
    });

    it('should return only direct dependencies (not transitive)', async () => {
      // Phase 1 -> Phase 2 -> Phase 3 (linear chain)
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      const phase3 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 3',
      });

      // Phase 2 depends on Phase 1
      await phaseRepo.createDependency(phase2.id, phase1.id);
      // Phase 3 depends on Phase 2
      await phaseRepo.createDependency(phase3.id, phase2.id);

      // Phase 3's dependencies should only include Phase 2, not Phase 1
      const depsPhase3 = await phaseRepo.getDependencies(phase3.id);
      expect(depsPhase3.length).toBe(1);
      expect(depsPhase3).toContain(phase2.id);
      expect(depsPhase3).not.toContain(phase1.id);
    });
  });

  describe('getDependents', () => {
    it('should return empty array for phase with no dependents', async () => {
      const phase = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });

      const dependents = await phaseRepo.getDependents(phase.id);
      expect(dependents).toEqual([]);
    });

    it('should return dependent phase IDs', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      const phase3 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 3',
      });

      // Phase 2 and Phase 3 both depend on Phase 1
      await phaseRepo.createDependency(phase2.id, phase1.id);
      await phaseRepo.createDependency(phase3.id, phase1.id);

      const dependents = await phaseRepo.getDependents(phase1.id);
      expect(dependents.length).toBe(2);
      expect(dependents).toContain(phase2.id);
      expect(dependents).toContain(phase3.id);
    });

    it('should return only direct dependents', async () => {
      // Phase 1 -> Phase 2 -> Phase 3 (linear chain)
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      const phase3 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 3',
      });

      // Phase 2 depends on Phase 1
      await phaseRepo.createDependency(phase2.id, phase1.id);
      // Phase 3 depends on Phase 2
      await phaseRepo.createDependency(phase3.id, phase2.id);

      // Phase 1's dependents should only include Phase 2, not Phase 3
      const dependentsPhase1 = await phaseRepo.getDependents(phase1.id);
      expect(dependentsPhase1.length).toBe(1);
      expect(dependentsPhase1).toContain(phase2.id);
      expect(dependentsPhase1).not.toContain(phase3.id);
    });
  });

  describe('findDependenciesByInitiativeId', () => {
    it('should return empty array for initiative with no dependencies', async () => {
      await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });

      const deps = await phaseRepo.findDependenciesByInitiativeId(testInitiativeId);
      expect(deps).toEqual([]);
    });

    it('should return all dependency edges for an initiative', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      const phase3 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 3',
      });

      await phaseRepo.createDependency(phase2.id, phase1.id);
      await phaseRepo.createDependency(phase3.id, phase1.id);
      await phaseRepo.createDependency(phase3.id, phase2.id);

      const deps = await phaseRepo.findDependenciesByInitiativeId(testInitiativeId);
      expect(deps.length).toBe(3);
      expect(deps).toContainEqual({ phaseId: phase2.id, dependsOnPhaseId: phase1.id });
      expect(deps).toContainEqual({ phaseId: phase3.id, dependsOnPhaseId: phase1.id });
      expect(deps).toContainEqual({ phaseId: phase3.id, dependsOnPhaseId: phase2.id });
    });

    it('should not return dependencies from other initiatives', async () => {
      const phase1 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepo.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      await phaseRepo.createDependency(phase2.id, phase1.id);

      const otherInitiative = await initiativeRepo.create({ name: 'Other' });
      const otherPhase1 = await phaseRepo.create({
        initiativeId: otherInitiative.id,
        name: 'Other Phase 1',
      });
      const otherPhase2 = await phaseRepo.create({
        initiativeId: otherInitiative.id,
        name: 'Other Phase 2',
      });
      await phaseRepo.createDependency(otherPhase2.id, otherPhase1.id);

      const deps = await phaseRepo.findDependenciesByInitiativeId(testInitiativeId);
      expect(deps.length).toBe(1);
      expect(deps[0].phaseId).toBe(phase2.id);
      expect(deps[0].dependsOnPhaseId).toBe(phase1.id);
    });
  });
});
