import { BaseRule } from '../../src/authorization/rules/BaseRule.js';
import { Arbiter } from '../../src/core/Arbiter.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';

// Create a concrete implementation of BaseRule for testing
class TestRule extends BaseRule {
  constructor(arbiter) {
    super(arbiter);
  }

  canBatchProcess() {
    return true;
  }

  _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
    // Simple test implementation
    return {
      possibility: 0.8,
      reliability: 0.9,
      collectedValues: [
        {
          value: 100,
          possibility: 0.8,
          path: [userKey, objectKey],
          source: {
            entityKey: userKey,
            relation: 'test',
            step: 1
          },
          metadata: {
            timestamp: Date.now(),
            reliability: 0.9,
            decay: null
          }
        }
      ],
      meta: {
        ruleType: 'test',
        userKey,
        objectKey
      },
      reason: 'test_evaluation'
    };
  }
}

describe('BaseRule - Core Rule Infrastructure', () => {
  let testRule;
  let arbiter;

  beforeEach(() => {
    arbiter = new Arbiter({ embeddingDimensions: 256 });
    testRule = new TestRule(arbiter);
    
    // Set up test entities
    arbiter.addNode('user:alice', 'user');
    arbiter.addNode('project:secret', 'project');
  });

  describe('Constructor and Initialization', () => {
    it('prevents direct instantiation of BaseRule', () => {
      assert.throws(() => {
        new BaseRule(arbiter);
      }, Error, 'BaseRule is abstract and cannot be instantiated directly');
    });
  });

  describe('Standard Evaluation Interface', () => {
    it('evaluates rules with proper input validation', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'test' };
      const visited = new Set();
      
      const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
      
      assert.ok(result, 'Should return result');
      assert.strictEqual(result.possibility, 0.8, 'Should return correct possibility');
      assert.strictEqual(result.reliability, 0.9, 'Should return correct reliability');
      assert.ok(Array.isArray(result.collectedValues), 'Should return collected values');
      assert.strictEqual(result.reason, 'test_evaluation', 'Should return correct reason');
    });

    it('tracks performance metrics', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'test' };
      const visited = new Set();
      
      testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
      
      assert.strictEqual(testRule.evaluationCount, 1, 'Should increment evaluation count');
      assert.ok(testRule.totalEvaluationTime >= 0, 'Should track evaluation time');
    });

    it('handles invalid inputs gracefully', () => {
      const result = testRule.evaluate(null, null, null, null, null, null, null);
      
      assert.ok(result, 'Should return error result');
      assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for invalid input');
      assert.ok(result.reason.includes('missing'), 'Should indicate error in reason');
    });
  });

  describe('Input Validation', () => {
    it('validates required parameters', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'test' };
      const visited = new Set();
      
      // Valid inputs should pass
      const validation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', {});
      assert.ok(validation.valid, 'Should validate valid inputs');
      
      // Invalid inputs should fail
      const invalidValidation = testRule._validateInputs(null, null, null, null, null, null, null, {});
      assert.strictEqual(invalidValidation.valid, false, 'Should reject invalid inputs');
    });

    it('validates rule configuration', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const visited = new Set();
      
      // Missing rule type - BaseRule doesn't validate rule type, just that it's an object
      const noTypeValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', {}, visited, 'test_relation', {});
      assert.ok(noTypeValidation.valid, 'Should accept rule without type');
      
      // Valid rule
      const validRule = { type: 'test' };
      const validValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', validRule, visited, 'test_relation', {});
      assert.ok(validValidation.valid, 'Should accept valid rule');
    });
  });

  describe('Options Normalization', () => {
    it('normalizes evaluation options', () => {
      const options = {
        fastPath: true,
        minAllowPossibility: 0.8,
        trackEvaluation: true
      };
      
      const normalized = testRule._normalizeOptions(options, { type: 'test' });
      
      assert.strictEqual(normalized.fastPath, true, 'Should preserve fastPath');
      assert.strictEqual(normalized.minAllowPossibility, 0.8, 'Should preserve minAllowPossibility');
      assert.strictEqual(normalized.trackEvaluation, true, 'Should preserve trackEvaluation');
    });

    it('provides default values for missing options', () => {
      const normalized = testRule._normalizeOptions({}, { type: 'test' });
      
      assert.strictEqual(normalized.fastPath, false, 'Should default fastPath to false');
      assert.strictEqual(normalized.minPossibility, null, 'Should default minPossibility to null');
      assert.strictEqual(normalized.trackEvaluation, true, 'Should default trackEvaluation to true');
    });
  });

  describe('Early Exit Optimization', () => {
    it('supports early exit with minAllowPossibility', () => {
      const options = {
        minAllowPossibility: 0.9,
        fastPath: true
      };
      
      const earlyExit = testRule._checkEarlyExit(options, { type: 'test' });
      
      // BaseRule doesn't implement early exit by default
      assert.strictEqual(earlyExit, null, 'Should return null for no early exit');
    });

    it('supports early exit with maxDenyPossibility', () => {
      const options = {
        maxDenyPossibility: 0.1,
        fastPath: true
      };
      
      const earlyExit = testRule._checkEarlyExit(options, { type: 'test' });
      
      // BaseRule doesn't implement early exit by default
      assert.strictEqual(earlyExit, null, 'Should return null for no early exit');
    });
  });

  describe('Result Post-Processing', () => {
    it('post-processes rule evaluation results', () => {
      const rawResult = {
        possibility: 0.8,
        reliability: 0.9,
        collectedValues: [],
        meta: { ruleType: 'test' },
        reason: 'test'
      };
      
      const rule = { type: 'test' };
      const options = { fastPath: false };
      
      const processed = testRule._postProcessResult(rawResult, rule, options);
      
      assert.strictEqual(processed.possibility, 0.8, 'Should preserve possibility');
      assert.strictEqual(processed.reliability, 0.9, 'Should preserve reliability');
      assert.ok(Array.isArray(processed.collectedValues), 'Should preserve collected values');
    });

    it('applies minimum rule possibility threshold', () => {
      const rawResult = {
        possibility: 0.3,
        reliability: 0.9,
        collectedValues: [],
        meta: { ruleType: 'test' },
        reason: 'test'
      };
      
      const rule = { type: 'test', minRulePossibility: 0.5 };
      const options = { fastPath: false };
      
      const processed = testRule._postProcessResult(rawResult, rule, options);
      
      // BaseRule doesn't apply minimum threshold by default
      assert.strictEqual(processed.possibility, 0.3, 'Should preserve original possibility');
    });
  });

  describe('Error Handling', () => {
    it('creates standardized error results', () => {
      const errorResult = testRule._createErrorResult('test_error', { details: 'test details' });
      
      assert.strictEqual(errorResult.possibility_allow, 0, 'Should return zero possibility for errors');
      assert.strictEqual(errorResult.reliability, 1.0, 'Should return reliability for errors');
      assert.strictEqual(errorResult.reason, 'test_error', 'Should include error reason');
      assert.ok(errorResult.details, 'Should include error details');
    });

    it('handles evaluation exceptions gracefully', () => {
      // Create a rule that throws an exception
      class ErrorRule extends BaseRule {
        constructor(arbiter) {
          super(arbiter);
        }
        
        _evaluateRule() {
          throw new Error('Test exception');
        }
      }
      
      const errorRule = new ErrorRule(arbiter);
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'error' };
      const visited = new Set();
      
      const result = errorRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
      
      assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for exceptions');
      assert.ok(result.reason.includes('error'), 'Should indicate error in reason');
    });
  });

  describe.skip('Batch Processing Support', () => {
    it('tracks batch evaluation performance', () => {
      const queries = [{
        userId: arbiter.nodeIdByKey.get('user:alice'),
        userKey: 'user:alice',
        objectId: arbiter.nodeIdByKey.get('project:secret'),
        objectKey: 'project:secret',
        rule: { type: 'test' },
        visited: new Set(),
        currentRelation: 'test_relation',
        options: {}
      }];
      
      testRule.batchEvaluate(queries);
      
      assert.strictEqual(testRule.batchEvaluationCount, 1, 'Should increment batch count');
      assert.ok(testRule.totalBatchEvaluationTime >= 0, 'Should track batch time');
    });
  });

  describe('Performance Metrics', () => {
    it('tracks evaluation statistics', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'test' };
      const visited = new Set();
      
      // Run multiple evaluations
      for (let i = 0; i < 5; i++) {
        testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
      }
      
      assert.strictEqual(testRule.evaluationCount, 5, 'Should track evaluation count');
      assert.ok(testRule.totalEvaluationTime >= 0, 'Should track total time');
    });

    it('provides performance summary', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'test' };
      const visited = new Set();
      
      testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
      
      const stats = testRule.getPerformanceStats();
      
      assert.strictEqual(stats.evaluationCount, 1, 'Should report evaluation count');
      assert.ok(stats.averageEvaluationTime >= 0, 'Should report average time');
    });
  });

  describe('Value Context Integration', () => {
    it('passes value context to rule evaluation', () => {
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'test' };
      const visited = new Set();
      const valueContext = { testValue: 42 };
      
      const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', { valueContext });
      
      assert.ok(result, 'Should handle value context');
      assert.strictEqual(result.possibility, 0.8, 'Should return correct result');
    });
  });

  describe('Abstract Method Requirements', () => {
    it('requires concrete implementations to define _evaluateRule', () => {
      class IncompleteRule extends BaseRule {
        constructor(arbiter) {
          super(arbiter);
        }
        // Missing _evaluateRule implementation
      }
      
      const incompleteRule = new IncompleteRule(arbiter);
      const userId = arbiter.nodeIdByKey.get('user:alice');
      const objectId = arbiter.nodeIdByKey.get('project:secret');
      const rule = { type: 'incomplete' };
      const visited = new Set();
      
      // Should handle missing implementation gracefully
      const result = incompleteRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
      assert.ok(result, 'Should return result even with missing implementation');
    });
  });
});
