import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ADRManager } from '../adr-manager.js';
import { ADRStore } from '../store.js';
import { ConfigManager } from '../../../config/config-manager.js';
import type {
  ADR,
  CreateADROptions,
  UpdateADROptions,
  ADRMetrics,
  ADRRelationship
} from '../types.js';

// Mock the ADRStore
vi.mock('../store.js', () => ({
  ADRStore: vi.fn().mockImplementation(() => ({
    initialize: vi.fn().mockResolvedValue(undefined),
    getNextADRNumber: vi.fn().mockResolvedValue('0001'),
    saveADR: vi.fn().mockResolvedValue(undefined),
    loadADR: vi.fn(),
    listADRs: vi.fn().mockResolvedValue([]),
    deleteADR: vi.fn().mockResolvedValue(undefined),
    searchADRs: vi.fn().mockResolvedValue([]),
    saveIndex: vi.fn().mockResolvedValue(undefined),
    loadIndex: vi.fn().mockResolvedValue(null),
    exportToMarkdown: vi.fn().mockReturnValue('# ADR'),
    generateADRLog: vi.fn().mockResolvedValue('# ADR Log')
  }))
}));

describe('ADRManager', () => {
  let manager: ADRManager;
  let mockStore: any;
  let configManager: ConfigManager;

  const testADR: ADR = {
    id: 'ADR-0001',
    title: 'Use TypeScript',
    status: 'accepted',
    date: new Date('2024-01-15'),
    deciders: ['John Doe'],
    template: 'nygard',
    context: 'Need type safety',
    decision: 'Use TypeScript',
    consequences: 'Learning curve',
    createdAt: new Date('2024-01-15'),
    updatedAt: new Date('2024-01-15'),
    statusHistory: []
  };

  beforeEach(async () => {
    configManager = new ConfigManager();
    manager = new ADRManager(configManager);
    mockStore = (manager as any).store;
    await manager.initialize();
  });

  describe('createADR', () => {
    it('should create a new ADR with auto-generated ID', async () => {
      const options: CreateADROptions = {
        title: 'Use React for Frontend',
        template: 'nygard',
        deciders: ['Frontend Team'],
        context: 'Need a modern UI framework',
        decision: 'We will use React',
        consequences: 'Team needs React training'
      };

      mockStore.getNextADRNumber.mockResolvedValueOnce('0002');

      const adr = await manager.createADR(options);

      expect(adr.id).toBe('ADR-0002');
      expect(adr.title).toBe(options.title);
      expect(adr.status).toBe('proposed');
      expect(adr.template).toBe('nygard');
      expect(adr.createdAt).toBeInstanceOf(Date);
      expect(adr.updatedAt).toBeInstanceOf(Date);
      expect(mockStore.saveADR).toHaveBeenCalledWith(adr);
    });

    it('should create ADR with MADR template fields', async () => {
      const options: CreateADROptions = {
        title: 'Choose Database System',
        template: 'madr',
        deciders: ['Backend Team'],
        context: 'Need to select a database',
        decision: 'Use PostgreSQL',
        consequences: 'Need PostgreSQL expertise',
        decisionDrivers: ['ACID compliance', 'Performance'],
        consideredOptions: [
          {
            title: 'PostgreSQL',
            description: 'Open source relational database',
            pros: ['ACID compliant', 'Mature'],
            cons: ['Complex setup']
          },
          {
            title: 'MongoDB',
            description: 'NoSQL document database',
            pros: ['Flexible schema', 'Easy scaling'],
            cons: ['No ACID', 'Different paradigm']
          }
        ]
      };

      const adr = await manager.createADR(options);

      expect(adr.decisionDrivers).toEqual(options.decisionDrivers);
      expect(adr.consideredOptions).toHaveLength(2);
      expect(adr.consideredOptions![0].title).toBe('PostgreSQL');
    });

    it('should add tags if provided', async () => {
      const options: CreateADROptions = {
        title: 'Use Docker for Containerization',
        template: 'nygard',
        deciders: ['DevOps Team'],
        context: 'Need consistent deployment',
        decision: 'Use Docker',
        consequences: 'Need Docker knowledge',
        tags: ['infrastructure', 'deployment', 'containers']
      };

      const adr = await manager.createADR(options);

      expect(adr.tags).toEqual(['infrastructure', 'deployment', 'containers']);
    });

    it('should update index after creation', async () => {
      const options: CreateADROptions = {
        title: 'Test ADR',
        template: 'nygard',
        deciders: ['Test Team'],
        context: 'Test context',
        decision: 'Test decision',
        consequences: 'Test consequences'
      };

      await manager.createADR(options);

      expect(mockStore.saveIndex).toHaveBeenCalled();
    });
  });

  describe('updateADR', () => {
    beforeEach(() => {
      // Return a fresh copy of testADR each time to avoid mutation issues
      mockStore.loadADR.mockImplementation(() => Promise.resolve({
        ...testADR,
        statusHistory: []
      }));
    });

    it('should update ADR fields', async () => {
      const updates: UpdateADROptions = {
        status: 'deprecated',
        consequences: 'Updated consequences',
        tags: ['legacy']
      };

      const updated = await manager.updateADR('ADR-0001', updates);

      expect(updated.status).toBe('deprecated');
      expect(updated.consequences).toBe('Updated consequences');
      expect(updated.tags).toEqual(['legacy']);
      expect(updated.updatedAt.getTime()).toBeGreaterThan(new Date('2024-01-15').getTime());
      expect(mockStore.saveADR).toHaveBeenCalledWith(updated);
    });

    it('should track status changes in history', async () => {
      const updates: UpdateADROptions = {
        status: 'rejected',
        statusChangeReason: 'Better alternative found',
        statusChangedBy: 'Jane Smith'
      };

      const updated = await manager.updateADR('ADR-0001', updates);

      expect(updated.statusHistory).toHaveLength(1);
      expect(updated.statusHistory[0]).toEqual({
        from: 'accepted',
        to: 'rejected',
        date: expect.any(Date),
        changedBy: 'Jane Smith',
        reason: 'Better alternative found'
      });
    });

    it('should handle superseding relationships', async () => {
      // Reset the mock for this specific test
      mockStore.loadADR.mockReset();
      mockStore.loadADR
        .mockResolvedValueOnce({ ...testADR, statusHistory: [] }) // Original ADR
        .mockResolvedValueOnce({        // Superseded ADR
          ...testADR,
          id: 'ADR-0000',
          status: 'accepted',
          statusHistory: []
        });

      const updates: UpdateADROptions = {
        status: 'accepted',
        supersedes: ['ADR-0000']
      };

      const updated = await manager.updateADR('ADR-0001', updates);

      expect(updated.supersedes).toEqual(['ADR-0000']);
      
      // Should update the superseded ADR
      expect(mockStore.saveADR).toHaveBeenCalledTimes(2);
      // First call is the main ADR, second is the superseded one
      const calls = mockStore.saveADR.mock.calls;
      const mainADRCall = calls.find(call => call[0].id === 'ADR-0001');
      const supersededCall = calls.find(call => call[0].id === 'ADR-0000');
      
      expect(supersededCall[0].status).toBe('superseded');
      expect(supersededCall[0].supersededBy).toBe('ADR-0001');
    });

    it('should throw error if ADR not found', async () => {
      mockStore.loadADR.mockResolvedValueOnce(null);

      await expect(manager.updateADR('ADR-9999', { status: 'rejected' }))
        .rejects.toThrow('ADR ADR-9999 not found');
    });
  });

  describe('linkADRs', () => {
    const adr1: ADR = { ...testADR, id: 'ADR-0001', relatedTo: [] };
    const adr2: ADR = { ...testADR, id: 'ADR-0002', relatedTo: [] };

    beforeEach(() => {
      mockStore.loadADR
        .mockResolvedValueOnce(adr1)
        .mockResolvedValueOnce(adr2);
    });

    it('should create bidirectional links', async () => {
      await manager.linkADRs('ADR-0001', 'ADR-0002', 'related-to');

      expect(mockStore.saveADR).toHaveBeenCalledTimes(2);
      
      const savedADR1 = mockStore.saveADR.mock.calls[0][0];
      const savedADR2 = mockStore.saveADR.mock.calls[1][0];
      
      expect(savedADR1.relatedTo).toContain('ADR-0002');
      expect(savedADR2.relatedTo).toContain('ADR-0001');
    });

    it('should handle supersedes relationship', async () => {
      mockStore.loadADR.mockReset();
      mockStore.loadADR
        .mockResolvedValueOnce({ ...adr1, status: 'proposed' })
        .mockResolvedValueOnce({ ...adr2, status: 'accepted' });

      await manager.linkADRs('ADR-0001', 'ADR-0002', 'supersedes');

      const savedADR1 = mockStore.saveADR.mock.calls[0][0];
      const savedADR2 = mockStore.saveADR.mock.calls[1][0];
      
      expect(savedADR1.supersedes).toContain('ADR-0002');
      expect(savedADR2.supersededBy).toBe('ADR-0001');
      expect(savedADR2.status).toBe('superseded');
    });

    it('should prevent duplicate links', async () => {
      mockStore.loadADR.mockReset();
      mockStore.loadADR
        .mockResolvedValueOnce({ ...adr1, relatedTo: ['ADR-0002'] })
        .mockResolvedValueOnce({ ...adr2, relatedTo: ['ADR-0001'] });

      await manager.linkADRs('ADR-0001', 'ADR-0002', 'related-to');

      // Should not save since link already exists
      expect(mockStore.saveADR).not.toHaveBeenCalled();
    });
  });

  describe('getADR', () => {
    it('should return ADR with formatted content', async () => {
      mockStore.loadADR.mockResolvedValueOnce(testADR);
      mockStore.exportToMarkdown.mockReturnValueOnce('# ADR-0001: Use TypeScript');

      const result = await manager.getADR('ADR-0001');

      expect(result).toBeDefined();
      expect(result!.adr).toEqual(testADR);
      expect(result!.markdown).toBe('# ADR-0001: Use TypeScript');
    });

    it('should return null if ADR not found', async () => {
      mockStore.loadADR.mockResolvedValueOnce(null);

      const result = await manager.getADR('ADR-9999');
      expect(result).toBeNull();
    });

    it('should include relationships', async () => {
      const adrWithLinks: ADR = {
        ...testADR,
        relatedTo: ['ADR-0002', 'ADR-0003'],
        supersedes: ['ADR-0000']
      };
      mockStore.loadADR.mockResolvedValueOnce(adrWithLinks);

      const result = await manager.getADR('ADR-0001');

      expect(result!.relationships).toHaveLength(3);
      expect(result!.relationships).toContainEqual({
        type: 'related-to',
        from: 'ADR-0001',
        to: 'ADR-0002'
      });
      expect(result!.relationships).toContainEqual({
        type: 'supersedes',
        from: 'ADR-0001',
        to: 'ADR-0000'
      });
    });
  });

  describe('listADRs', () => {
    it('should return list of ADRs with filters', async () => {
      const adrs = [
        { ...testADR, id: 'ADR-0001', status: 'accepted' as const },
        { ...testADR, id: 'ADR-0002', status: 'proposed' as const },
        { ...testADR, id: 'ADR-0003', status: 'rejected' as const }
      ];

      mockStore.listADRs.mockResolvedValueOnce(['ADR-0001', 'ADR-0002', 'ADR-0003']);
      mockStore.loadADR
        .mockResolvedValueOnce(adrs[0])
        .mockResolvedValueOnce(adrs[1])
        .mockResolvedValueOnce(adrs[2]);

      const result = await manager.listADRs({ status: 'accepted' });

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('ADR-0001');
    });

    it('should return all ADRs when no filter provided', async () => {
      mockStore.listADRs.mockResolvedValueOnce(['ADR-0001', 'ADR-0002']);
      mockStore.loadADR
        .mockResolvedValueOnce(testADR)
        .mockResolvedValueOnce({ ...testADR, id: 'ADR-0002' });

      const result = await manager.listADRs();

      expect(result).toHaveLength(2);
    });

    it('should sort by date descending', async () => {
      const adr1 = { ...testADR, id: 'ADR-0001', date: new Date('2024-01-01') };
      const adr2 = { ...testADR, id: 'ADR-0002', date: new Date('2024-02-01') };

      mockStore.listADRs.mockResolvedValueOnce(['ADR-0001', 'ADR-0002']);
      mockStore.loadADR
        .mockResolvedValueOnce(adr1)
        .mockResolvedValueOnce(adr2);

      const result = await manager.listADRs();

      expect(result[0].id).toBe('ADR-0002'); // More recent first
      expect(result[1].id).toBe('ADR-0001');
    });
  });

  describe('searchADRs', () => {
    it('should delegate to store search', async () => {
      const searchResults = [testADR];
      mockStore.searchADRs.mockResolvedValueOnce(searchResults);

      const results = await manager.searchADRs({
        query: 'typescript',
        tags: ['frontend']
      });

      expect(results).toEqual(searchResults);
      expect(mockStore.searchADRs).toHaveBeenCalledWith({
        query: 'typescript',
        tags: ['frontend']
      });
    });
  });

  describe('getMetrics', () => {
    it('should calculate ADR metrics', async () => {
      const adrs = [
        { ...testADR, id: 'ADR-0001', status: 'accepted' as const, deciders: ['John'] },
        { ...testADR, id: 'ADR-0002', status: 'accepted' as const, deciders: ['John', 'Jane'] },
        { ...testADR, id: 'ADR-0003', status: 'proposed' as const, deciders: ['Jane'] },
        { ...testADR, id: 'ADR-0004', status: 'rejected' as const, deciders: ['John'] },
        { ...testADR, id: 'ADR-0005', status: 'deprecated' as const, template: 'madr' as const }
      ];

      mockStore.listADRs.mockResolvedValueOnce(adrs.map(a => a.id));
      adrs.forEach(adr => {
        mockStore.loadADR.mockResolvedValueOnce(adr);
      });

      const metrics = await manager.getMetrics();

      expect(metrics.total).toBe(5);
      expect(metrics.byStatus.accepted).toBe(2);
      expect(metrics.byStatus.proposed).toBe(1);
      expect(metrics.byStatus.rejected).toBe(1);
      expect(metrics.byStatus.deprecated).toBe(1);
      expect(metrics.byTemplate.nygard).toBe(4);
      expect(metrics.byTemplate.madr).toBe(1);
      
      expect(metrics.mostActiveDeciders).toContainEqual({ name: 'John', count: 3 });
      expect(metrics.mostActiveDeciders).toContainEqual({ name: 'Jane', count: 2 });
    });

    it('should calculate average decision time', async () => {
      const adrs = [
        {
          ...testADR,
          id: 'ADR-0001',
          status: 'accepted' as const,
          statusHistory: [{
            from: 'proposed' as const,
            to: 'accepted' as const,
            date: new Date('2024-01-20'),
            changedBy: 'John',
            reason: 'Approved'
          }],
          createdAt: new Date('2024-01-15')
        },
        {
          ...testADR,
          id: 'ADR-0002',
          status: 'accepted' as const,
          statusHistory: [{
            from: 'proposed' as const,
            to: 'accepted' as const,
            date: new Date('2024-01-25'),
            changedBy: 'Jane',
            reason: 'Approved'
          }],
          createdAt: new Date('2024-01-15')
        }
      ];

      mockStore.listADRs.mockResolvedValueOnce(adrs.map(a => a.id));
      adrs.forEach(adr => {
        mockStore.loadADR.mockResolvedValueOnce(adr);
      });

      const metrics = await manager.getMetrics();

      // ADR-0001: 5 days, ADR-0002: 10 days, average: 7.5 days
      expect(metrics.averageDecisionTime).toBe(7.5);
    });
  });

  describe('validateReferences', () => {
    it('should find broken references', async () => {
      const adrWithBrokenRefs: ADR = {
        ...testADR,
        id: 'ADR-0001',
        relatedTo: ['ADR-0002', 'ADR-9999'], // ADR-9999 doesn't exist
        supersedes: ['ADR-0000', 'ADR-8888'] // ADR-8888 doesn't exist
      };

      mockStore.listADRs.mockResolvedValueOnce(['ADR-0001']);
      mockStore.loadADR
        .mockResolvedValueOnce(adrWithBrokenRefs)
        .mockResolvedValueOnce({ ...testADR, id: 'ADR-0002' })
        .mockResolvedValueOnce(null) // ADR-9999
        .mockResolvedValueOnce({ ...testADR, id: 'ADR-0000' })
        .mockResolvedValueOnce(null); // ADR-8888

      const validation = await manager.validateReferences();

      expect(validation.valid).toBe(false);
      expect(validation.errors).toHaveLength(2);
      expect(validation.errors[0]).toEqual({
        adrId: 'ADR-0001',
        type: 'related-to',
        targetId: 'ADR-9999',
        error: 'Target ADR not found'
      });
      expect(validation.errors[1]).toEqual({
        adrId: 'ADR-0001',
        type: 'supersedes',
        targetId: 'ADR-8888',
        error: 'Target ADR not found'
      });
    });

    it('should validate all references are valid', async () => {
      const adr: ADR = {
        ...testADR,
        id: 'ADR-0001',
        relatedTo: ['ADR-0002'],
        supersedes: ['ADR-0000']
      };

      mockStore.listADRs.mockResolvedValueOnce(['ADR-0001']);
      mockStore.loadADR
        .mockResolvedValueOnce(adr)
        .mockResolvedValueOnce({ ...testADR, id: 'ADR-0002' })
        .mockResolvedValueOnce({ ...testADR, id: 'ADR-0000' });

      const validation = await manager.validateReferences();

      expect(validation.valid).toBe(true);
      expect(validation.errors).toHaveLength(0);
    });
  });

  describe('generateDecisionLog', () => {
    it('should generate formatted decision log', async () => {
      mockStore.generateADRLog.mockResolvedValueOnce('# Architecture Decision Log\n...');

      const log = await manager.generateDecisionLog();

      expect(log).toBe('# Architecture Decision Log\n...');
      expect(mockStore.generateADRLog).toHaveBeenCalled();
    });
  });
});