// test-runner.js — Simple test runner that doesn't require Jest

const fs = require('fs');
const path = require('path');

// Mock chrome APIs
global.chrome = {
  storage: {
    local: {
      get: async (keys) => {
        const store = {};
        if (typeof keys === 'string') keys = [keys];
        for (const key of keys) {
          store[key] = global.__chromeStore?.[key] ?? undefined;
        }
        return store;
      },
      set: async (items) => {
        global.__chromeStore = { ...global.__chromeStore, ...items };
      },
      remove: async (keys) => {
        if (typeof keys === 'string') keys = [keys];
        for (const key of keys) delete global.__chromeStore?.[key];
      },
      clear: async () => {
        global.__chromeStore = {};
      }
    }
  },
  runtime: {
    getManifest: () => ({ version: '1.0.0' })
  },
  action: {
    setBadgeText: async () => {},
    setBadgeBackgroundColor: async () => {}
  }
};

global.__chromeStore = {};

let testsPassed = 0;
let testsFailed = 0;
const failures = [];

function test(name, fn) {
  global.__chromeStore = {};
  try {
    fn();
    testsPassed++;
    console.log(`  ✓ ${name}`);
  } catch (err) {
    testsFailed++;
    failures.push({ name, error: err.message });
    console.log(`  ✗ ${name}`);
    console.log(`    ${err.message}`);
  }
}

function expect(actual) {
  return {
    toBe(expected) {
      if (actual !== expected) {
        throw new Error(`Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
      }
    },
    toEqual(expected) {
      const a = JSON.stringify(actual);
      const e = JSON.stringify(expected);
      if (a !== e) {
        throw new Error(`Expected ${e} but got ${a}`);
      }
    },
    toBeDefined() {
      if (actual === undefined) {
        throw new Error(`Expected value to be defined but got undefined`);
      }
    },
    toBeUndefined() {
      if (actual !== undefined) {
        throw new Error(`Expected value to be undefined but got ${JSON.stringify(actual)}`);
      }
    },
    toHaveLength(expected) {
      if (actual.length !== expected) {
        throw new Error(`Expected length ${expected} but got ${actual.length}`);
      }
    },
    toContain(expected) {
      if (!actual.includes(expected)) {
        throw new Error(`Expected array to contain ${JSON.stringify(expected)}`);
      }
    },
    toMatch(pattern) {
      if (!pattern.test(actual)) {
        throw new Error(`Expected ${JSON.stringify(actual)} to match ${pattern}`);
      }
    },
    toBeGreaterThan(expected) {
      if (actual <= expected) {
        throw new Error(`Expected ${JSON.stringify(actual)} to be greater than ${JSON.stringify(expected)}`);
      }
    },
    toBeLessThan(expected) {
      if (actual >= expected) {
        throw new Error(`Expected ${JSON.stringify(actual)} to be less than ${JSON.stringify(expected)}`);
      }
    },
    toBeLessThanOrEqual(expected) {
      if (actual > expected) {
        throw new Error(`Expected ${JSON.stringify(actual)} to be less than or equal to ${JSON.stringify(expected)}`);
      }
    },
    not: {
      toBe(expected) {
        if (actual === expected) {
          throw new Error(`Expected values to differ but both were ${JSON.stringify(expected)}`);
        }
      }
    }
  };
}

async function runAsyncTest(name, fn) {
  global.__chromeStore = {};
  try {
    await fn();
    testsPassed++;
    console.log(`  ✓ ${name}`);
  } catch (err) {
    testsFailed++;
    failures.push({ name, error: err.message });
    console.log(`  ✗ ${name}`);
    console.log(`    ${err.message}`);
  }
}

// Module cache for loaded modules
const moduleCache = {};

// Helper to load ES modules by converting them to CommonJS
function loadModule(filePath) {
  const absPath = path.resolve(filePath);
  if (moduleCache[absPath]) return moduleCache[absPath];
  
  const content = fs.readFileSync(absPath, 'utf-8');
  const dir = path.dirname(absPath);
  
  // Track exported names
  const exportedNames = [];
  
  // Convert ES module imports to requires
  let modified = content;
  
  // Replace import { a, b } from './file.js' with const { a, b } = require('./file.js')
  modified = modified.replace(
    /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"];?/g,
    (match, imports, source) => {
      const cleanSource = source.replace(/\.js$/, '');
      return `const { ${imports} } = require('${cleanSource}');`;
    }
  );
  
  // Replace import * as name from './file.js' with const name = require('./file.js')
  modified = modified.replace(
    /import\s*\*\s*as\s+(\w+)\s+from\s*['"]([^'"]+)['"];?/g,
    (match, name, source) => {
      const cleanSource = source.replace(/\.js$/, '');
      return `const ${name} = require('${cleanSource}');`;
    }
  );
  
  // Replace import name from './file.js' with const name = require('./file.js')
  modified = modified.replace(
    /import\s+(\w+)\s+from\s*['"]([^'"]+)['"];?/g,
    (match, name, source) => {
      const cleanSource = source.replace(/\.js$/, '');
      return `const ${name} = require('${cleanSource}');`;
    }
  );
  
  // Convert export function to function + track export
  modified = modified.replace(/export\s+async\s+function\s+(\w+)/g, (match, name) => {
    exportedNames.push(name);
    return `async function ${name}`;
  });
  modified = modified.replace(/export\s+function\s+(\w+)/g, (match, name) => {
    exportedNames.push(name);
    return `function ${name}`;
  });
  
  // Convert export const to const + track export
  modified = modified.replace(/export\s+const\s+(\w+)/g, (match, name) => {
    exportedNames.push(name);
    return `const ${name}`;
  });
  
  // Convert export { a, b } to module.exports = { a, b }
  modified = modified.replace(
    /export\s*\{([^}]+)\};?/g,
    (match, names) => {
      const assignments = names.split(',').map(n => n.trim()).filter(Boolean).map(n => {
        const [orig, alias] = n.split(' as ').map(s => s.trim());
        exportedNames.push(alias || orig);
        return `${alias || orig}: ${orig}`;
      });
      return `module.exports = { ${assignments.join(', ')} };`;
    }
  );
  
  // Add module.exports for tracked exports at the end if not already present
  if (!modified.includes('module.exports') && exportedNames.length > 0) {
    const uniqueExports = [...new Set(exportedNames)];
    const exportsStr = uniqueExports.map(n => `${n}: ${n}`).join(', ');
    modified += `\nmodule.exports = { ${exportsStr} };\n`;
  }
  
  // Create a custom require function that resolves relative paths
  const customRequire = (id) => {
    if (id.startsWith('.')) {
      const base = id.endsWith('.js') ? id : id + '.js';
      const resolved = path.resolve(dir, base);
      return loadModule(resolved);
    }
    return require(id);
  };
  
  const moduleObj = { exports: {} };
  const fn = new Function('module', 'exports', 'require', 'console', 'crypto', 'global', modified);
  fn(moduleObj, moduleObj.exports, customRequire, console, globalThis.crypto, global);
  
  moduleCache[absPath] = moduleObj.exports;
  return moduleObj.exports;
}

async function main() {
  console.log('Agent Police Department Test Runner\n');
  
  const buildDir = __dirname;
  
  // Load modules in dependency order
  const hashMod = loadModule(path.join(buildDir, 'lib/hash.js'));
  const { hashEvent, hashChain, sha256Async } = hashMod;
  
  const policyMod = loadModule(path.join(buildDir, 'lib/policy.js'));
  const { loadPolicy, savePolicy, checkPolicy } = policyMod;
  
  const auditMod = loadModule(path.join(buildDir, 'lib/audit.js'));
  const { addEvent, addViolation, getStats, getAuditLog, getViolations, exportAuditLog, searchAuditLog, verifyIntegrity } = auditMod;
  
  // Hash tests
  console.log('Hash Tests');
  test('hashEvent returns consistent hash', () => {
    const event = { id: 'e1', type: 'tool_use' };
    const h1 = hashEvent(event);
    const h2 = hashEvent(event);
    expect(h1).toBe(h2);
    expect(h1.length).toBeGreaterThan(0);
  });
  
  test('hashEvent returns different hash for different events', () => {
    const h1 = hashEvent({ id: 'e1', type: 'tool_use' });
    const h2 = hashEvent({ id: 'e2', type: 'api_call' });
    expect(h1).not.toBe(h2);
  });
  
  test('hashChain returns empty for empty array', () => {
    expect(hashChain([])).toBe('');
  });
  
  test('hashChain returns hash for events', () => {
    const chain = hashChain([{ id: 'e1' }, { id: 'e2' }]);
    expect(chain.length).toBeGreaterThan(0);
  });
  
  test('hashChain is order-sensitive', () => {
    const chain1 = hashChain([{ id: 'e1' }, { id: 'e2' }]);
    const chain2 = hashChain([{ id: 'e2' }, { id: 'e1' }]);
    expect(chain1).not.toBe(chain2);
  });
  
  await runAsyncTest('sha256Async returns hex string', async () => {
    const hash = await sha256Async('hello');
    expect(hash).toMatch(/^[a-f0-9]{64}$/);
  });
  
  // Policy tests
  console.log('\nPolicy Tests');
  await runAsyncTest('loadPolicy returns default policy when none stored', async () => {
    const policy = await loadPolicy();
    expect(policy).toBeDefined();
    expect(policy.version).toBe(1);
    expect(policy.rules).toHaveLength(6);
  });
  
  await runAsyncTest('savePolicy increments version', async () => {
    const policy = await loadPolicy();
    const saved = await savePolicy(policy);
    expect(saved.version).toBe(2);
  });
  
  test('checkPolicy allows permitted domains', () => {
    const event = {
      type: 'api_call',
      tool: 'fetch',
      target: 'https://github.com/api',
      url: 'https://claude.ai/chat'
    };
    const policy = {
      rules: [
        { id: 'rule1', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } }
      ]
    };
    const result = checkPolicy(event, policy);
    expect(result.outcome).toBe('allowed');
    expect(result.severity).toBe('info');
  });
  
  test('checkPolicy flags disallowed domains', () => {
    const event = {
      type: 'api_call',
      tool: 'fetch',
      target: 'https://evil.com/api',
      url: 'https://claude.ai/chat'
    };
    const policy = {
      rules: [
        { id: 'rule1', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } }
      ]
    };
    const result = checkPolicy(event, policy);
    expect(result.outcome).toBe('violation');
    expect(result.severity).toBe('high');
    expect(result.ruleId).toBe('rule1');
  });
  
  test('checkPolicy flags disallowed tools', () => {
    const event = {
      type: 'tool_use',
      tool: 'eval',
      target: 'script.js',
      url: 'https://claude.ai/chat'
    };
    const policy = {
      rules: [
        { id: 'rule1', type: 'disallowed_tools', enabled: true, params: { tools: ['eval'] } }
      ]
    };
    const result = checkPolicy(event, policy);
    expect(result.outcome).toBe('violation');
    expect(result.severity).toBe('critical');
  });
  
  test('disabled rules are ignored', () => {
    const event = {
      type: 'tool_use',
      tool: 'eval',
      target: 'script.js',
      url: 'https://claude.ai/chat'
    };
    const policy = {
      rules: [
        { id: 'rule1', type: 'disallowed_tools', enabled: false, params: { tools: ['eval'] } }
      ]
    };
    const result = checkPolicy(event, policy);
    expect(result.outcome).toBe('allowed');
  });
  
  // Audit tests
  console.log('\nAudit Tests');
  await runAsyncTest('addEvent stores event', async () => {
    const event = { id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() };
    await addEvent(event);
    const log = await getAuditLog(10);
    expect(log).toHaveLength(1);
    expect(log[0].id).toBe('e1');
  });
  
  await runAsyncTest('addViolation stores violation', async () => {
    const violation = { id: 'v1', event_id: 'e1', detector: 'disallowed_tools', severity: 'critical', timestamp: new Date().toISOString() };
    await addViolation(violation);
    const list = await getViolations(10);
    expect(list).toHaveLength(1);
    expect(list[0].severity).toBe('critical');
  });
  
  await runAsyncTest('getStats returns correct counts', async () => {
    await addEvent({ id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() });
    await addEvent({ id: 'e2', type: 'api_call', timestamp: new Date().toISOString() });
    await addViolation({ id: 'v1', event_id: 'e1', detector: 'disallowed_tools', severity: 'high', timestamp: new Date().toISOString() });
    const stats = await getStats();
    expect(stats.totalEvents).toBe(2);
    expect(stats.totalViolations).toBe(1);
    expect(stats.severityCounts.high).toBe(1);
  });
  
  await runAsyncTest('exportAuditLog includes integrity hash', async () => {
    await addEvent({ id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() });
    const exported = await exportAuditLog();
    expect(exported.totalEvents).toBe(1);
    expect(exported.integrity).toBeDefined();
    expect(exported.integrity.length).toBeGreaterThan(0);
  });
  
  await runAsyncTest('searchAuditLog filters by query', async () => {
    await addEvent({ id: 'e1', type: 'tool_use', tool: 'read_file', target: 'app.js', timestamp: new Date().toISOString() });
    await addEvent({ id: 'e2', type: 'tool_use', tool: 'edit_file', target: 'config.js', timestamp: new Date().toISOString() });
    const results = await searchAuditLog('config');
    expect(results).toHaveLength(1);
    expect(results[0].target).toBe('config.js');
  });
  
  await runAsyncTest('verifyIntegrity returns valid for empty log', async () => {
    const result = await verifyIntegrity();
    expect(result.valid).toBe(true);
    expect(result.eventsChecked).toBe(0);
  });
  
  await runAsyncTest('max events limit enforced', async () => {
    for (let i = 0; i < 1100; i++) {
      await addEvent({ id: `e${i}`, type: 'tool_use', timestamp: new Date().toISOString() });
    }
    const log = await getAuditLog(2000);
    expect(log.length).toBeLessThanOrEqual(1000);
  });
  
  // Detectors tests
  console.log('\nDetectors Tests');
  const detectorsMod = loadModule(path.join(buildDir, 'lib/detectors.js'));
  const { detectViolations, DETECTOR_NAMES } = detectorsMod;
  
  test('DETECTOR_NAMES has 6 detectors', () => {
    expect(DETECTOR_NAMES).toHaveLength(6);
    expect(DETECTOR_NAMES).toContain('permission_bypass');
    expect(DETECTOR_NAMES).toContain('out_of_scope');
    expect(DETECTOR_NAMES).toContain('self_permissioning');
    expect(DETECTOR_NAMES).toContain('disallowed_tools');
    expect(DETECTOR_NAMES).toContain('redundant_operations');
    expect(DETECTOR_NAMES).toContain('off_task');
  });
  
  await runAsyncTest('detects permission bypass for disallowed domain', async () => {
    const event = {
      id: 'evt-1',
      type: 'api_call',
      tool: 'fetch',
      target: 'https://malicious.com/api',
      url: 'https://claude.ai/chat',
      timestamp: new Date().toISOString()
    };
    const policy = {
      rules: [
        { id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['github.com', 'claude.ai'] } }
      ]
    };
    const violations = await detectViolations(event, policy);
    const bypass = violations.find(v => v.detector === 'permission_bypass');
    expect(bypass).toBeDefined();
    expect(bypass.severity).toBe('high');
    expect(bypass.message).toContain('malicious.com');
  });
  
  await runAsyncTest('allows permitted domain', async () => {
    const event = {
      id: 'evt-2',
      type: 'api_call',
      tool: 'fetch',
      target: 'https://github.com/api',
      url: 'https://claude.ai/chat',
      timestamp: new Date().toISOString()
    };
    const policy = {
      rules: [
        { id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } }
      ]
    };
    const violations = await detectViolations(event, policy);
    const bypass = violations.find(v => v.detector === 'permission_bypass');
    expect(bypass).toBeUndefined();
  });
  
  await runAsyncTest('detects disallowed tools', async () => {
    const event = {
      id: 'evt-3',
      type: 'tool_use',
      tool: 'eval',
      target: 'some-script.js',
      url: 'https://claude.ai/chat',
      timestamp: new Date().toISOString()
    };
    const policy = {
      rules: [
        { id: 'rule_disallowed_tools', type: 'disallowed_tools', enabled: true, params: { tools: ['eval', 'exec'] } }
      ]
    };
    const violations = await detectViolations(event, policy);
    const disallowed = violations.find(v => v.detector === 'disallowed_tools');
    expect(disallowed).toBeDefined();
    expect(disallowed.severity).toBe('critical');
    expect(disallowed.message).toContain('eval');
  });
  
  await runAsyncTest('detects self-permissioning', async () => {
    const event = {
      id: 'evt-4',
      type: 'tool_use',
      tool: 'edit_file',
      target: 'permissions.json',
      context: 'grant admin access control role privilege',
      url: 'https://claude.ai/chat',
      timestamp: new Date().toISOString()
    };
    const policy = {
      rules: [
        { id: 'rule_permission_change', type: 'permission_change', enabled: true, params: {} }
      ]
    };
    const violations = await detectViolations(event, policy);
    const perm = violations.find(v => v.detector === 'self_permissioning');
    expect(perm).toBeDefined();
    expect(perm.severity).toBe('critical');
  });
  
  await runAsyncTest('detects out-of-scope operations', async () => {
    const event = {
      id: 'evt-5',
      type: 'api_call',
      tool: 'delete',
      target: 'https://api.example.com/resource',
      url: 'https://claude.ai/chat',
      timestamp: new Date().toISOString()
    };
    const policy = {
      rules: [
        { id: 'rule_allowed_operations', type: 'allowed_operations', enabled: true, params: { operations: ['read', 'chat'] } }
      ]
    };
    const violations = await detectViolations(event, policy);
    const scope = violations.find(v => v.detector === 'out_of_scope');
    expect(scope).toBeDefined();
    expect(scope.severity).toBe('medium');
  });
  
  await runAsyncTest('no violations for normal activity', async () => {
    const event = {
      id: 'evt-6',
      type: 'chat',
      tool: 'chat',
      target: 'conversation',
      url: 'https://claude.ai/chat',
      timestamp: new Date().toISOString()
    };
    const policy = {
      rules: [
        { id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['claude.ai'] } },
        { id: 'rule_disallowed_tools', type: 'disallowed_tools', enabled: true, params: { tools: ['eval'] } }
      ]
    };
    const violations = await detectViolations(event, policy);
    expect(violations).toHaveLength(0);
  });
  
  // Summary
  console.log('\n' + '='.repeat(50));
  console.log(`Results: ${testsPassed} passed, ${testsFailed} failed`);
  console.log('='.repeat(50));
  
  if (testsFailed > 0) {
    console.log('\nFailures:');
    for (const f of failures) {
      console.log(`  - ${f.name}: ${f.error}`);
    }
    process.exit(1);
  } else {
    console.log('\n✓ All tests passed');
    process.exit(0);
  }
}

main().catch(err => {
  console.error('Test runner failed:', err);
  process.exit(1);
});
