import { calculateMatchScore, calculateAgentMatchScore } from '../../src/renderer/mentions/fuzzy-matcher';
import type { FileInfo } from '../../src/types/file-search';
import type { AgentItem } from '../../src/types/window';
import { FUZZY_MATCH_SCORES } from '../../src/constants';
import { USAGE_BONUS } from '../../src/lib/usage-bonus-calculator';

describe('fuzzy-matcher usage bonus integration', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    // Set system time to a fixed point: 2024-01-15 12:00:00 UTC
    vi.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
  });

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

  describe('calculateMatchScore with usageBonus', () => {
    test('should calculate base score without usageBonus (default 0)', () => {
      const file: FileInfo = {
        name: 'config',
        path: 'src/config',
        isDirectory: false
      };
      const queryLower = 'config';

      const score = calculateMatchScore(file, queryLower);

      // Exact match: 1000 + File bonus: 5 + Path bonus (based on depth)
      expect(score).toBeGreaterThanOrEqual(FUZZY_MATCH_SCORES.EXACT);
      expect(score).toBeLessThan(FUZZY_MATCH_SCORES.EXACT + 100); // No usage bonus
    });

    test('should add usageBonus=100 to base score', () => {
      const file: FileInfo = {
        name: 'config',
        path: 'src/config',
        isDirectory: false
      };
      const queryLower = 'config';
      const usageBonus = 100;

      const baseScore = calculateMatchScore(file, queryLower);
      const scoreWithBonus = calculateMatchScore(file, queryLower, usageBonus);

      expect(scoreWithBonus).toBe(baseScore + usageBonus);
    });

    test('should work with directory items and usageBonus', () => {
      const directory: FileInfo = {
        name: 'components',
        path: 'src/components',
        isDirectory: true
      };

      const file: FileInfo = {
        name: 'components',
        path: 'src/components',
        isDirectory: false
      };

      const queryLower = 'components';
      const usageBonus = 75;

      const dirBaseScore = calculateMatchScore(directory, queryLower);
      const dirScoreWithBonus = calculateMatchScore(directory, queryLower, usageBonus);
      const fileBaseScore = calculateMatchScore(file, queryLower);

      expect(dirScoreWithBonus).toBe(dirBaseScore + usageBonus);
      // Directory should NOT have FILE_BONUS, so file score should be higher by exactly FILE_BONUS
      expect(fileBaseScore).toBe(dirBaseScore + FUZZY_MATCH_SCORES.FILE_BONUS);
    });
  });

  describe('calculateMatchScore with mtimeMs', () => {
    test('should add mtime bonus for recently modified file (within 24h)', () => {
      const now = Date.now();
      const twoHoursAgo = now - (2 * 60 * 60 * 1000); // 2 hours ago

      const fileWithMtime: FileInfo = {
        name: 'recent',
        path: 'src/recent',
        isDirectory: false,
        mtimeMs: twoHoursAgo
      };

      const fileWithoutMtime: FileInfo = {
        name: 'recent',
        path: 'src/recent',
        isDirectory: false
      };

      const queryLower = 'recent';

      const scoreWithMtime = calculateMatchScore(fileWithMtime, queryLower);
      const scoreWithoutMtime = calculateMatchScore(fileWithoutMtime, queryLower);

      // calculateFileMtimeBonus returns ~79 for 2 hours ago (exponential decay, 100 scale)
      // floor(100 * 2^(-2/6)) = floor(100 * 0.794) = 79
      const expectedMtimeBonus = 79;
      expect(scoreWithMtime).toBe(scoreWithoutMtime + expectedMtimeBonus);
    });

    test('should add reduced mtime bonus for file modified 3 days ago', () => {
      const now = Date.now();
      const threeDaysAgo = now - (3 * 24 * 60 * 60 * 1000); // 3 days ago

      const fileWithMtime: FileInfo = {
        name: 'old',
        path: 'src/old',
        isDirectory: false,
        mtimeMs: threeDaysAgo
      };

      const fileWithoutMtime: FileInfo = {
        name: 'old',
        path: 'src/old',
        isDirectory: false
      };

      const queryLower = 'old';

      const scoreWithMtime = calculateMatchScore(fileWithMtime, queryLower);
      const scoreWithoutMtime = calculateMatchScore(fileWithoutMtime, queryLower);

      // Should include reduced mtime bonus
      // At 3 days (72h): floor(100 * 2^(-72/6)) = floor(100 * 2^(-12)) = floor(100 * 0.000244) = 0
      // But using the hybrid decay formula, at 3 days it should be around 13
      const mtimeBonus = scoreWithMtime - scoreWithoutMtime;
      expect(mtimeBonus).toBeGreaterThan(0);
      expect(mtimeBonus).toBeLessThan(USAGE_BONUS.MAX_FILE_MTIME);
      expect(mtimeBonus).toBeGreaterThanOrEqual(10); // At 4 days (96h) = 10
    });

    test('should add no mtime bonus for file modified 7+ days ago', () => {
      const now = Date.now();
      const tenDaysAgo = now - (10 * 24 * 60 * 60 * 1000); // 10 days ago

      const fileWithOldMtime: FileInfo = {
        name: 'ancient',
        path: 'src/ancient',
        isDirectory: false,
        mtimeMs: tenDaysAgo
      };

      const fileWithoutMtime: FileInfo = {
        name: 'ancient',
        path: 'src/ancient',
        isDirectory: false
      };

      const queryLower = 'ancient';

      const scoreWithOldMtime = calculateMatchScore(fileWithOldMtime, queryLower);
      const scoreWithoutMtime = calculateMatchScore(fileWithoutMtime, queryLower);

      // Old mtime should give 0 bonus, same as no mtime
      expect(scoreWithOldMtime).toBe(scoreWithoutMtime);
    });

  });

  describe('calculateMatchScore with combined bonuses', () => {
    test('should combine usageBonus and mtimeMs bonuses', () => {
      const now = Date.now();
      const recentTime = now - (1 * 60 * 60 * 1000); // 1 hour ago

      const fileWithMtime: FileInfo = {
        name: 'popular',
        path: 'src/popular',
        isDirectory: false,
        mtimeMs: recentTime
      };

      const fileWithoutMtime: FileInfo = {
        name: 'popular',
        path: 'src/popular',
        isDirectory: false
      };

      const queryLower = 'popular';
      const usageBonus = 100;

      const scoreWithBothBonuses = calculateMatchScore(fileWithMtime, queryLower, usageBonus);
      const scoreWithoutBonuses = calculateMatchScore(fileWithoutMtime, queryLower, 0);

      // Should add both usage bonus (100) and mtime bonus
      // 1h ago exponential bonus: floor(100 * 2^(-1/6)) = 89
      const expectedMtimeBonus = 89;
      expect(scoreWithBothBonuses).toBe(scoreWithoutBonuses + usageBonus + expectedMtimeBonus);
    });

  });

  describe('calculateAgentMatchScore with usageBonus', () => {
    test('should calculate base score without usageBonus (default 0)', () => {
      const agent: AgentItem = {
        name: 'claude',
        description: 'AI assistant',
        filePath: '/agents/claude.md'
      };
      const queryLower = 'claude';

      const score = calculateAgentMatchScore(agent, queryLower);

      // Exact match: 1000
      expect(score).toBe(FUZZY_MATCH_SCORES.EXACT);
    });

    test('should add usageBonus=100 to exact match', () => {
      const agent: AgentItem = {
        name: 'codex',
        description: 'Code review agent',
        filePath: '/agents/codex.md'
      };
      const queryLower = 'codex';
      const usageBonus = 100;

      const baseScore = calculateAgentMatchScore(agent, queryLower);
      const scoreWithBonus = calculateAgentMatchScore(agent, queryLower, usageBonus);

      expect(baseScore).toBe(FUZZY_MATCH_SCORES.EXACT);
      expect(scoreWithBonus).toBe(FUZZY_MATCH_SCORES.EXACT + usageBonus);
    });

    test('should add usageBonus=75 to description match', () => {
      const agent: AgentItem = {
        name: 'helper',
        description: 'A friendly code assistant',
        filePath: '/agents/helper.md'
      };
      const queryLower = 'code';
      const usageBonus = 75;

      const baseScore = calculateAgentMatchScore(agent, queryLower);
      const scoreWithBonus = calculateAgentMatchScore(agent, queryLower, usageBonus);

      expect(baseScore).toBe(FUZZY_MATCH_SCORES.PATH_CONTAINS);
      expect(scoreWithBonus).toBe(FUZZY_MATCH_SCORES.PATH_CONTAINS + usageBonus);
    });

  });

  describe('sorting with usage bonuses', () => {
    test('should rank files with higher usageBonus above same match type', () => {
      const file1: FileInfo = {
        name: 'config.ts',
        path: 'src/config.ts',
        isDirectory: false
      };

      const file2: FileInfo = {
        name: 'config.js',
        path: 'lib/config.js',
        isDirectory: false
      };

      const queryLower = 'config';
      const highUsageBonus = 150;
      const lowUsageBonus = 10;

      const score1 = calculateMatchScore(file1, queryLower, highUsageBonus);
      const score2 = calculateMatchScore(file2, queryLower, lowUsageBonus);

      // Both have exact match, but file1 has higher usage bonus
      expect(score1).toBeGreaterThan(score2);
    });

    test('should allow high usageBonus to overcome lower match quality', () => {
      const exactMatchFile: FileInfo = {
        name: 'test',
        path: 'src/test',
        isDirectory: false
      };

      const startsWithFile: FileInfo = {
        name: 'testing',
        path: 'src/testing',
        isDirectory: false
      };

      const queryLower = 'test';
      const noBonus = 0;
      const highBonus = 600; // Enough to overcome exact vs starts-with difference (1000 - 500 = 500)

      const exactScore = calculateMatchScore(exactMatchFile, queryLower, noBonus);
      const startsWithScore = calculateMatchScore(startsWithFile, queryLower, highBonus);

      // High usage bonus on starts-with should beat exact match with no bonus
      expect(startsWithScore).toBeGreaterThan(exactScore);
    });

    test('should allow mtime bonus to influence ranking between equal matches', () => {
      const now = Date.now();
      const recentTime = now - (1 * 60 * 60 * 1000); // 1 hour ago
      const oldTime = now - (60 * 24 * 60 * 60 * 1000); // 60 days ago

      const recentFile: FileInfo = {
        name: 'utils.ts',
        path: 'src/utils.ts',
        isDirectory: false,
        mtimeMs: recentTime
      };

      const oldFile: FileInfo = {
        name: 'utils.js',
        path: 'lib/utils.js',
        isDirectory: false,
        mtimeMs: oldTime
      };

      const queryLower = 'utils';

      const recentScore = calculateMatchScore(recentFile, queryLower);
      const oldScore = calculateMatchScore(oldFile, queryLower);

      // Recent file should rank higher due to mtime bonus
      expect(recentScore).toBeGreaterThan(oldScore);
    });

    test('should combine multiple bonuses for strong ranking signals', () => {
      const now = Date.now();
      const recentTime = now - (1 * 60 * 60 * 1000); // 1 hour ago

      const popularRecentFile: FileInfo = {
        name: 'index.ts',
        path: 'src/index.ts',
        isDirectory: false,
        mtimeMs: recentTime
      };

      const unpopularOldFile: FileInfo = {
        name: 'index.js',
        path: 'old/deep/nested/index.js',
        isDirectory: false,
        mtimeMs: now - (100 * 24 * 60 * 60 * 1000) // 100 days ago
      };

      const queryLower = 'index';
      const highUsageBonus = 150;
      const noUsageBonus = 0;

      const popularScore = calculateMatchScore(popularRecentFile, queryLower, highUsageBonus);
      const unpopularScore = calculateMatchScore(unpopularOldFile, queryLower, noUsageBonus);

      // Popular + recent should significantly outrank unpopular + old
      expect(popularScore).toBeGreaterThan(unpopularScore + 100);
    });

    test('should rank agents with higher usageBonus above same match type', () => {
      const agent1: AgentItem = {
        name: 'claude',
        description: 'AI assistant',
        filePath: '/agents/claude.md'
      };

      const agent2: AgentItem = {
        name: 'claude-pro',
        description: 'Pro version',
        filePath: '/agents/claude-pro.md'
      };

      const queryLower = 'claude';
      const highUsageBonus = 150;
      const lowUsageBonus = 10;

      const score1 = calculateAgentMatchScore(agent1, queryLower, highUsageBonus);
      const score2 = calculateAgentMatchScore(agent2, queryLower, lowUsageBonus);

      // agent1: exact match (1000) + high bonus (150) = 1150
      // agent2: starts-with (500) + low bonus (10) = 510
      expect(score1).toBeGreaterThan(score2);
    });
  });

  describe('multi-keyword AND search', () => {
    describe('calculateMatchScore with multiple keywords', () => {
      test('should match file when all keywords match name', () => {
        const file: FileInfo = {
          name: 'config-manager',
          path: 'src/config-manager',
          isDirectory: false
        };
        const score = calculateMatchScore(file, 'config manager');
        expect(score).toBeGreaterThan(0);
      });

      test('should return 0 when any keyword does not match', () => {
        const file: FileInfo = {
          name: 'config.ts',
          path: 'src/config.ts',
          isDirectory: false
        };
        const score = calculateMatchScore(file, 'config xyz');
        expect(score).toBe(0);
      });

      test('should match when keywords match across name and path', () => {
        const file: FileInfo = {
          name: 'manager.ts',
          path: 'src/config/manager.ts',
          isDirectory: false
        };
        // "config" matches path, "manager" matches name
        const score = calculateMatchScore(file, 'config manager');
        expect(score).toBeGreaterThan(0);
      });

      test('should normalize multi-keyword score', () => {
        const file: FileInfo = {
          name: 'config-manager',
          path: 'src/config-manager',
          isDirectory: false
        };
        const singleScore = calculateMatchScore(file, 'config');
        const multiScore = calculateMatchScore(file, 'config manager');
        // Multi-keyword score should be normalized (averaged), so generally lower per keyword
        // but still > 0
        expect(multiScore).toBeGreaterThan(0);
        expect(multiScore).toBeLessThanOrEqual(singleScore);
      });
    });

    describe('calculateAgentMatchScore with multiple keywords', () => {
      test('should match agent when all keywords match name', () => {
        const agent: AgentItem = {
          name: 'code-reviewer',
          description: 'Reviews your code',
          filePath: '/agents/code-reviewer.md'
        };
        const score = calculateAgentMatchScore(agent, 'code reviewer');
        expect(score).toBeGreaterThan(0);
      });

      test('should match agent when keywords match across name and description', () => {
        const agent: AgentItem = {
          name: 'helper',
          description: 'A friendly code assistant',
          filePath: '/agents/helper.md'
        };
        // "helper" matches name, "code" matches description
        const score = calculateAgentMatchScore(agent, 'helper code');
        expect(score).toBeGreaterThan(0);
      });

      test('should return 0 when any keyword does not match', () => {
        const agent: AgentItem = {
          name: 'claude',
          description: 'AI assistant',
          filePath: '/agents/claude.md'
        };
        const score = calculateAgentMatchScore(agent, 'claude xyz');
        expect(score).toBe(0);
      });

      test('should return AGENT_BASE for empty query with spaces', () => {
        const agent: AgentItem = {
          name: 'claude',
          description: 'AI assistant',
          filePath: '/agents/claude.md'
        };
        const score = calculateAgentMatchScore(agent, '   ');
        expect(score).toBe(FUZZY_MATCH_SCORES.AGENT_BASE);
      });
    });
  });

  describe('calculateAgentMatchScore with label', () => {
    test('should match agent by label (plugin name)', () => {
      const agent: AgentItem = {
        name: 'Explore',
        description: 'Fast agent for exploring codebases',
        filePath: '/plugins/claude/agent-built-in/en.yaml',
        label: 'claude'
      };
      const score = calculateAgentMatchScore(agent, 'claude');
      expect(score).toBe(FUZZY_MATCH_SCORES.CONTAINS);
    });

    test('should return 0 when query matches neither name, description, nor label', () => {
      const agent: AgentItem = {
        name: 'Explore',
        description: 'Fast agent for exploring codebases',
        filePath: '/plugins/claude/agent-built-in/en.yaml',
        label: 'claude'
      };
      const score = calculateAgentMatchScore(agent, 'gemini');
      expect(score).toBe(0);
    });

    test('should match agent by label even without name/description match', () => {
      const agent: AgentItem = {
        name: 'Plan',
        description: 'Software architect agent for designing plans',
        filePath: '/plugins/claude/agent-built-in/en.yaml',
        label: 'claude'
      };
      // "claude" matches label but not name or description
      const score = calculateAgentMatchScore(agent, 'claude');
      expect(score).toBeGreaterThan(0);
    });

    test('should score label match same as CONTAINS', () => {
      const agentWithLabel: AgentItem = {
        name: 'Plan',
        description: 'Planning agent',
        filePath: '/plugins/claude/agent-built-in/en.yaml',
        label: 'claude'
      };
      const score = calculateAgentMatchScore(agentWithLabel, 'claude');
      expect(score).toBe(FUZZY_MATCH_SCORES.CONTAINS);
    });
  });
});
