// Must mock crypto-utils BEFORE auth-manager is required,
// because auth-manager.js line 13: const JWT_SECRET = cryptoUtils.loadOrCreateKey()
const mockFixedKey = Buffer.alloc(32, 'jwt-test-key-pad');
jest.mock('../src/security/crypto-utils', () => ({
  loadOrCreateKey: jest.fn(() => mockFixedKey),
}));

jest.mock('../src/managers/credential-manager', () => ({
  store: jest.fn().mockResolvedValue(true),
  retrieve: jest.fn().mockResolvedValue(null),
  delete: jest.fn().mockResolvedValue(true),
  list: jest.fn().mockResolvedValue([]),
}));

const crypto = require('crypto');
const authManager = require('../src/managers/auth-manager');
const credentialManager = require('../src/managers/credential-manager');

describe('AuthManager', () => {
  beforeEach(() => {
    authManager.clearCache();
    jest.clearAllMocks();
  });

  describe('JWT Generation and Verification', () => {
    it('generateJWT returns a valid JWT string', async () => {
      const token = await authManager.generateJWT({ sub: 'user1' });
      expect(typeof token).toBe('string');
      expect(token.split('.')).toHaveLength(3); // header.payload.signature
    });

    it('generateJWT defaults scope to [read, write]', async () => {
      const token = await authManager.generateJWT({ sub: 'user1' });
      const result = await authManager.verifyJWT(token);
      expect(result.scope).toEqual(['read', 'write']);
    });

    it('generateJWT respects custom scope', async () => {
      const token = await authManager.generateJWT({ sub: 'user1', scope: ['admin'] });
      const result = await authManager.verifyJWT(token);
      expect(result.scope).toEqual(['admin']);
    });

    it('generateJWT throws if payload.sub missing', async () => {
      await expect(authManager.generateJWT({ name: 'test' }))
        .rejects.toThrow('must include "sub"');
    });

    it('generateJWT respects custom expiresIn', async () => {
      const token = await authManager.generateJWT({ sub: 'user1' }, '1s');
      // Token should be valid immediately
      const result = await authManager.verifyJWT(token);
      expect(result).not.toBeNull();
    });

    it('verifyJWT returns decoded payload for valid token', async () => {
      const token = await authManager.generateJWT({ sub: 'user1' });
      const result = await authManager.verifyJWT(token);
      expect(result).not.toBeNull();
      expect(result.userId).toBe('user1');
      expect(result.scope).toEqual(['read', 'write']);
      expect(result.iat).toBeDefined();
      expect(result.exp).toBeDefined();
    });

    it('verifyJWT returns null for expired token', async () => {
      const token = await authManager.generateJWT({ sub: 'user1' }, '0s');
      // Wait a tick for expiration
      await new Promise(r => setTimeout(r, 50));
      const result = await authManager.verifyJWT(token);
      expect(result).toBeNull();
    });

    it('verifyJWT returns null for invalid token', async () => {
      const result = await authManager.verifyJWT('garbage.not.ajwt');
      expect(result).toBeNull();
    });

    it('verifyJWT returns null for token signed with different secret', async () => {
      const jwt = require('jsonwebtoken');
      const fakeToken = jwt.sign({ sub: 'user1' }, 'wrong-secret');
      const result = await authManager.verifyJWT(fakeToken);
      expect(result).toBeNull();
    });
  });

  describe('API Key Generation', () => {
    it('generateAPIKey returns key in dk_<id>_<secret> format', async () => {
      const result = await authManager.generateAPIKey('My Key');
      expect(result.key).toMatch(/^dk_[a-f0-9]+_[a-f0-9]+$/);
    });

    it('generateAPIKey stores SHA-256 hash via credentialManager', async () => {
      const result = await authManager.generateAPIKey('Test Key');
      expect(credentialManager.store).toHaveBeenCalledWith(
        expect.stringContaining('auth.apikey.'),
        expect.any(String) // SHA-256 hash
      );
    });

    it('generateAPIKey stores metadata separately', async () => {
      await authManager.generateAPIKey('Named Key', ['read']);
      // Second call should be metadata
      const metaCalls = credentialManager.store.mock.calls.filter(
        call => call[0].startsWith('auth.metadata.')
      );
      expect(metaCalls.length).toBe(1);
      const metadata = JSON.parse(metaCalls[0][1]);
      expect(metadata.name).toBe('Named Key');
      expect(metadata.scopes).toEqual(['read']);
    });

    it('generateAPIKey returns id, name, scopes, createdAt', async () => {
      const result = await authManager.generateAPIKey('Full Key', ['read', 'write']);
      expect(result).toHaveProperty('key');
      expect(result).toHaveProperty('id');
      expect(result.name).toBe('Full Key');
      expect(result.scopes).toEqual(['read', 'write']);
      expect(result.createdAt).toBeDefined();
    });

    it('generateAPIKey throws if name missing', async () => {
      await expect(authManager.generateAPIKey('')).rejects.toThrow('name is required');
    });

    it('generateAPIKey caches metadata', async () => {
      const result = await authManager.generateAPIKey('Cached Key');
      expect(authManager.keyMetadataCache.has(result.id)).toBe(true);
    });
  });

  describe('API Key Verification', () => {
    let testKey;
    let testKeyId;
    let testHash;

    beforeEach(async () => {
      // Generate a key for verification tests
      const generated = await authManager.generateAPIKey('Verify Test');
      testKey = generated.key;
      testKeyId = generated.id;
      testHash = crypto.createHash('sha256').update(testKey).digest('hex');

      // Set up credentialManager to return the hash and metadata
      credentialManager.retrieve.mockImplementation(async (key) => {
        if (key === `auth.apikey.${testKeyId}`) return testHash;
        if (key === `auth.metadata.${testKeyId}`) {
          return JSON.stringify({ id: testKeyId, name: 'Verify Test', scopes: ['read', 'write'] });
        }
        return null;
      });
    });

    it('verifyAPIKey returns keyId, scopes, name for valid key', async () => {
      // Clear cache to force credential lookup
      authManager.clearCache();
      const result = await authManager.verifyAPIKey(testKey);
      expect(result).not.toBeNull();
      expect(result.keyId).toBe(testKeyId);
      expect(result.scopes).toEqual(['read', 'write']);
      expect(result.name).toBe('Verify Test');
    });

    it('verifyAPIKey returns null for key not starting with dk_', async () => {
      const result = await authManager.verifyAPIKey('invalid_prefix_key');
      expect(result).toBeNull();
    });

    it('verifyAPIKey returns null for key with wrong part count', async () => {
      const result = await authManager.verifyAPIKey('dk_only_two');
      expect(result).toBeNull();
    });

    it('verifyAPIKey returns null when stored hash not found', async () => {
      credentialManager.retrieve.mockResolvedValue(null);
      authManager.clearCache();
      const result = await authManager.verifyAPIKey(`dk_${testKeyId}_wrongsecret`);
      expect(result).toBeNull();
    });

    it('verifyAPIKey returns null on hash mismatch', async () => {
      credentialManager.retrieve.mockImplementation(async (key) => {
        if (key.startsWith('auth.apikey.')) return 'wrong-hash-value-that-does-not-match';
        return null;
      });
      authManager.clearCache();
      // The hash comparison will fail because hashes have different lengths
      const result = await authManager.verifyAPIKey(testKey);
      expect(result).toBeNull();
    });

    it('verifyAPIKey returns null when metadata not found', async () => {
      credentialManager.retrieve.mockImplementation(async (key) => {
        if (key.startsWith('auth.apikey.')) return testHash;
        return null; // No metadata
      });
      authManager.clearCache();
      const result = await authManager.verifyAPIKey(testKey);
      expect(result).toBeNull();
    });
  });

  describe('API Key Revocation', () => {
    it('revokeAPIKey deletes hash and metadata', async () => {
      await authManager.revokeAPIKey('abc123');
      expect(credentialManager.delete).toHaveBeenCalledWith('auth.apikey.abc123');
      expect(credentialManager.delete).toHaveBeenCalledWith('auth.metadata.abc123');
    });

    it('revokeAPIKey removes from cache', async () => {
      authManager.keyMetadataCache.set('abc123', { name: 'test' });
      await authManager.revokeAPIKey('abc123');
      expect(authManager.keyMetadataCache.has('abc123')).toBe(false);
    });

    it('revokeAPIKey returns true on success', async () => {
      const result = await authManager.revokeAPIKey('test');
      expect(result).toBe(true);
    });

    it('revokeAPIKey returns false on error', async () => {
      credentialManager.delete.mockRejectedValueOnce(new Error('fail'));
      const result = await authManager.revokeAPIKey('fail-key');
      expect(result).toBe(false);
    });
  });

  describe('API Key Listing', () => {
    it('listAPIKeys returns metadata for all keys', async () => {
      credentialManager.list.mockResolvedValue([
        'auth.metadata.key1',
        'auth.metadata.key2',
        'auth.apikey.key1',
        'auth.apikey.key2'
      ]);
      credentialManager.retrieve.mockImplementation(async (key) => {
        if (key === 'auth.metadata.key1') return JSON.stringify({ id: 'key1', name: 'Key 1' });
        if (key === 'auth.metadata.key2') return JSON.stringify({ id: 'key2', name: 'Key 2' });
        return null;
      });

      const keys = await authManager.listAPIKeys();
      expect(keys).toHaveLength(2);
      expect(keys[0].name).toBe('Key 1');
      expect(keys[1].name).toBe('Key 2');
    });

    it('listAPIKeys returns empty array on error', async () => {
      credentialManager.list.mockRejectedValue(new Error('fail'));
      const keys = await authManager.listAPIKeys();
      expect(keys).toEqual([]);
    });
  });

  describe('Key Metadata', () => {
    it('getKeyMetadata returns from cache when available', async () => {
      authManager.keyMetadataCache.set('cached', { name: 'Cached' });
      const result = await authManager.getKeyMetadata('cached');
      expect(result.name).toBe('Cached');
      expect(credentialManager.retrieve).not.toHaveBeenCalled();
    });

    it('getKeyMetadata fetches from credentialManager when not cached', async () => {
      credentialManager.retrieve.mockResolvedValue(JSON.stringify({ id: 'x', name: 'Fetched' }));
      const result = await authManager.getKeyMetadata('x');
      expect(result.name).toBe('Fetched');
      expect(credentialManager.retrieve).toHaveBeenCalledWith('auth.metadata.x');
    });

    it('getKeyMetadata caches fetched result', async () => {
      credentialManager.retrieve.mockResolvedValue(JSON.stringify({ id: 'y', name: 'Cached Now' }));
      await authManager.getKeyMetadata('y');
      expect(authManager.keyMetadataCache.has('y')).toBe(true);
    });

    it('getKeyMetadata returns null when not found', async () => {
      credentialManager.retrieve.mockResolvedValue(null);
      const result = await authManager.getKeyMetadata('missing');
      expect(result).toBeNull();
    });
  });

  describe('Cache', () => {
    it('clearCache empties keyMetadataCache', () => {
      authManager.keyMetadataCache.set('a', { name: 'A' });
      authManager.keyMetadataCache.set('b', { name: 'B' });
      authManager.clearCache();
      expect(authManager.keyMetadataCache.size).toBe(0);
    });
  });
});
