import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { ADRStore } from '../store.js';
import { promises as fs } from 'fs';
import path from 'path';
import type { ADR, ADRStatus } from '../types.js';

// Mock the storage manager
vi.mock('../../../storage/storage-manager.js', () => ({
  StorageManager: vi.fn().mockImplementation(() => ({
    getStorageLocation: vi.fn().mockResolvedValue({
      data: '/tmp/test-atlas'
    })
  }))
}));

// Mock fs operations
vi.mock('fs', () => ({
  promises: {
    access: vi.fn(),
    mkdir: vi.fn().mockResolvedValue(undefined),
    readFile: vi.fn(),
    writeFile: vi.fn().mockResolvedValue(undefined),
    readdir: vi.fn(),
    unlink: vi.fn().mockResolvedValue(undefined)
  }
}));

describe('ADRStore', () => {
  let store: ADRStore;
  const mockFs = fs as any;

  const testADR: ADR = {
    id: 'ADR-0001',
    title: 'Use TypeScript for Type Safety',
    status: 'accepted',
    date: new Date('2024-01-15'),
    deciders: ['John Doe', 'Jane Smith'],
    template: 'nygard',
    context: 'We need type safety in our JavaScript codebase',
    decision: 'We will use TypeScript for all new code',
    consequences: 'Developers need to learn TypeScript; build process becomes more complex',
    tags: ['frontend', 'tooling'],
    createdAt: new Date('2024-01-15'),
    updatedAt: new Date('2024-01-15'),
    statusHistory: []
  };

  beforeEach(async () => {
    store = new ADRStore();
    await store.initialize();
    vi.clearAllMocks();
  });

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

  describe('initialization', () => {
    it('should initialize with correct paths', async () => {
      const newStore = new ADRStore();
      await newStore.initialize();
      
      expect(mockFs.mkdir).toHaveBeenCalled();
      const mkdirCalls = mockFs.mkdir.mock.calls;
      
      // Should create both records and templates directories
      expect(mkdirCalls.some(call => call[0].endsWith('adr/records'))).toBe(true);
      expect(mkdirCalls.some(call => call[0].endsWith('adr/templates'))).toBe(true);
    });

    it('should not initialize twice', async () => {
      const callCount = mockFs.mkdir.mock.calls.length;
      await store.initialize();
      expect(mockFs.mkdir).toHaveBeenCalledTimes(callCount);
    });
  });

  describe('saveADR and loadADR', () => {
    it('should save ADR to file', async () => {
      await store.saveADR(testADR);

      expect(mockFs.writeFile).toHaveBeenCalled();
      const writeCall = mockFs.writeFile.mock.calls[0];
      expect(writeCall[0]).toContain('ADR-0001-use-typescript-for-type-safety.json');
      expect(writeCall[1]).toBe(JSON.stringify(testADR, null, 2));
    });

    it('should generate correct filename from title', async () => {
      const adr: ADR = {
        ...testADR,
        id: 'ADR-0002',
        title: 'Adopt Microservices Architecture'
      };

      await store.saveADR(adr);

      const writeCall = mockFs.writeFile.mock.calls[0];
      expect(writeCall[0]).toContain('ADR-0002-adopt-microservices-architecture.json');
    });

    it('should load ADR from file', async () => {
      // First mock readdir to find the file
      mockFs.readdir.mockResolvedValueOnce(['ADR-0001-use-typescript-for-type-safety.json']);
      // Then mock readFile to return the content
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(testADR));

      const loaded = await store.loadADR('ADR-0001');

      expect(loaded).toBeDefined();
      expect(loaded!.id).toBe('ADR-0001');
      expect(loaded!.title).toBe('Use TypeScript for Type Safety');
      expect(loaded!.date).toBeInstanceOf(Date);
      expect(loaded!.createdAt).toBeInstanceOf(Date);
      expect(loaded!.updatedAt).toBeInstanceOf(Date);
    });

    it('should return null for non-existent ADR', async () => {
      mockFs.readdir.mockResolvedValueOnce([]); // No files found

      const loaded = await store.loadADR('ADR-9999');
      expect(loaded).toBeNull();
    });
  });

  describe('listADRs', () => {
    it('should list all ADR IDs', async () => {
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0001-use-typescript.json',
        'ADR-0002-adopt-microservices.json',
        'ADR-0003-use-postgresql.json',
        'index.json', // Should be ignored
        '.DS_Store'   // Should be ignored
      ]);

      const ids = await store.listADRs();

      expect(ids).toEqual(['ADR-0001', 'ADR-0002', 'ADR-0003']);
      expect(ids).not.toContain('index');
    });

    it('should return empty array when no ADRs exist', async () => {
      mockFs.readdir.mockRejectedValueOnce(new Error('Directory not found'));

      const ids = await store.listADRs();
      expect(ids).toEqual([]);
    });

    it('should sort ADRs numerically', async () => {
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0010-late-decision.json',
        'ADR-0002-early-decision.json',
        'ADR-0001-first-decision.json'
      ]);

      const ids = await store.listADRs();

      expect(ids).toEqual(['ADR-0001', 'ADR-0002', 'ADR-0010']);
    });
  });

  describe('getNextADRNumber', () => {
    it('should return 0001 when no ADRs exist', async () => {
      mockFs.readdir.mockRejectedValueOnce(new Error('Directory not found'));

      const next = await store.getNextADRNumber();
      expect(next).toBe('0001');
    });

    it('should return next number in sequence', async () => {
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0001-first.json',
        'ADR-0002-second.json',
        'ADR-0003-third.json'
      ]);

      const next = await store.getNextADRNumber();
      expect(next).toBe('0004');
    });

    it('should handle gaps in numbering', async () => {
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0001-first.json',
        'ADR-0003-third.json',
        'ADR-0007-seventh.json'
      ]);

      const next = await store.getNextADRNumber();
      expect(next).toBe('0008');
    });
  });

  describe('deleteADR', () => {
    it('should delete ADR file', async () => {
      // Mock for finding the file
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0001-use-typescript.json'
      ]);

      await store.deleteADR('ADR-0001');

      expect(mockFs.unlink).toHaveBeenCalled();
      const unlinkCall = mockFs.unlink.mock.calls[0];
      expect(unlinkCall[0]).toContain('ADR-0001-use-typescript.json');
    });

    it('should not throw error if file does not exist', async () => {
      mockFs.readdir.mockResolvedValueOnce([]);
      
      await expect(store.deleteADR('ADR-9999')).resolves.not.toThrow();
    });
  });

  describe('saveIndex and loadIndex', () => {
    const testIndex = {
      lastUpdated: new Date(),
      totalADRs: 3,
      adrs: [
        { id: 'ADR-0001', title: 'First ADR', status: 'accepted' as ADRStatus },
        { id: 'ADR-0002', title: 'Second ADR', status: 'proposed' as ADRStatus },
        { id: 'ADR-0003', title: 'Third ADR', status: 'rejected' as ADRStatus }
      ]
    };

    it('should save index file', async () => {
      await store.saveIndex(testIndex);

      expect(mockFs.writeFile).toHaveBeenCalled();
      const writeCall = mockFs.writeFile.mock.calls[0];
      expect(writeCall[0]).toContain('index.json');
      expect(JSON.parse(writeCall[1])).toEqual({
        ...testIndex,
        lastUpdated: testIndex.lastUpdated.toISOString()
      });
    });

    it('should load index file', async () => {
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify({
        ...testIndex,
        lastUpdated: testIndex.lastUpdated.toISOString()
      }));

      const loaded = await store.loadIndex();

      expect(loaded).toBeDefined();
      expect(loaded!.totalADRs).toBe(3);
      expect(loaded!.adrs).toHaveLength(3);
      expect(loaded!.lastUpdated).toBeInstanceOf(Date);
    });

    it('should return null if index does not exist', async () => {
      mockFs.readFile.mockRejectedValueOnce(new Error('File not found'));

      const loaded = await store.loadIndex();
      expect(loaded).toBeNull();
    });
  });

  describe('searchADRs', () => {
    const adr1: ADR = {
      ...testADR,
      id: 'ADR-0001',
      title: 'Use React for Frontend',
      context: 'Need a modern frontend framework',
      status: 'accepted',
      tags: ['frontend', 'react']
    };

    const adr2: ADR = {
      ...testADR,
      id: 'ADR-0002',
      title: 'Use PostgreSQL for Database',
      context: 'Need a reliable database',
      status: 'accepted',
      tags: ['backend', 'database']
    };

    const adr3: ADR = {
      ...testADR,
      id: 'ADR-0003',
      title: 'Adopt Microservices',
      context: 'Need to scale the backend',
      status: 'proposed',
      tags: ['architecture', 'backend']
    };

    beforeEach(() => {
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0001-use-react.json',
        'ADR-0002-use-postgresql.json',
        'ADR-0003-adopt-microservices.json'
      ]);
    });

    it('should search by query text', async () => {
      // Need to mock readdir calls for loadADR within search
      mockFs.readdir.mockResolvedValueOnce(['ADR-0001-use-react.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr1));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0002-use-postgresql.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr2));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0003-adopt-microservices.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr3));

      const results = await store.searchADRs({ query: 'database' });

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

    it('should search by status', async () => {
      mockFs.readdir.mockResolvedValueOnce(['ADR-0001-use-react.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr1));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0002-use-postgresql.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr2));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0003-adopt-microservices.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr3));

      const results = await store.searchADRs({ status: ['proposed'] });

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

    it('should search by tags', async () => {
      mockFs.readdir.mockResolvedValueOnce(['ADR-0001-use-react.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr1));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0002-use-postgresql.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr2));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0003-adopt-microservices.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr3));

      const results = await store.searchADRs({ tags: ['backend'] });

      expect(results).toHaveLength(2);
      expect(results.map(r => r.id)).toContain('ADR-0002');
      expect(results.map(r => r.id)).toContain('ADR-0003');
    });

    it('should combine search criteria', async () => {
      mockFs.readdir.mockResolvedValueOnce(['ADR-0001-use-react.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr1));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0002-use-postgresql.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr2));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0003-adopt-microservices.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify(adr3));

      const results = await store.searchADRs({
        query: 'backend',
        status: ['accepted']
      });

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

  describe('exportToMarkdown', () => {
    it('should export ADR to markdown format', async () => {
      const markdown = await store.exportToMarkdown(testADR);

      expect(markdown).toContain('# ADR-0001: Use TypeScript for Type Safety');
      expect(markdown).toContain('## Status');
      expect(markdown).toContain('Accepted');
      expect(markdown).toContain('## Context');
      expect(markdown).toContain(testADR.context);
      expect(markdown).toContain('## Decision');
      expect(markdown).toContain(testADR.decision);
      expect(markdown).toContain('## Consequences');
      expect(markdown).toContain(testADR.consequences);
    });

    it('should include optional fields when present', async () => {
      const adrWithOptions: ADR = {
        ...testADR,
        decisionDrivers: ['Performance', 'Developer Experience'],
        consideredOptions: [
          {
            title: 'JavaScript',
            description: 'Use plain JavaScript',
            pros: ['No build step'],
            cons: ['No type safety']
          }
        ],
        supersedes: ['ADR-0000']
      };

      const markdown = await store.exportToMarkdown(adrWithOptions);

      expect(markdown).toContain('## Decision Drivers');
      expect(markdown).toContain('Performance');
      expect(markdown).toContain('## Considered Options');
      expect(markdown).toContain('JavaScript');
      expect(markdown).toContain('## Links');
      expect(markdown).toContain('Supersedes: ADR-0000');
    });
  });

  describe('generateADRLog', () => {
    it('should generate a log of all ADRs', async () => {
      mockFs.readdir.mockResolvedValueOnce([
        'ADR-0001-first.json',
        'ADR-0002-second.json'
      ]);

      // Need to mock readdir for each loadADR call
      mockFs.readdir.mockResolvedValueOnce(['ADR-0001-first.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify({
        ...testADR,
        id: 'ADR-0001',
        title: 'First Decision'
      }));
      
      mockFs.readdir.mockResolvedValueOnce(['ADR-0002-second.json']);
      mockFs.readFile.mockResolvedValueOnce(JSON.stringify({
        ...testADR,
        id: 'ADR-0002',
        title: 'Second Decision',
        status: 'proposed'
      }));

      const log = await store.generateADRLog();

      expect(log).toContain('# Architecture Decision Records');
      expect(log).toContain('ADR-0001');
      expect(log).toContain('First Decision');
      expect(log).toContain('ADR-0002');
      expect(log).toContain('Second Decision');
      expect(log).toContain('Accepted');
      expect(log).toContain('Proposed');
    });
  });
});