/**
 * @fileoverview Tests for AnalysisRun aggregate
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { AnalysisRun } from '../analysis-run.aggregate.js';
import type {
  CreateAnalysisRunParams,
  IssueOccurrence,
  RunSummary,
  CommitInfo,
  RunTimestamps,
  RunStatus,
} from '../analysis-run.types.js';
import type {
  RunId,
  ProjectKey,
  GraphQLNodeId,
  CommitOid,
  BranchName,
  AnalyzerShortcode,
} from '../../../../types/branded.js';
import { IssueCount } from '../../../value-objects/issue-count.js';

describe('AnalysisRun Aggregate', () => {
  let validParams: CreateAnalysisRunParams;
  let runId: RunId;
  let projectKey: ProjectKey;
  let repositoryId: GraphQLNodeId;
  let commitInfo: CommitInfo;

  beforeEach(() => {
    runId = 'run-123' as RunId;
    projectKey = 'test-project' as ProjectKey;
    repositoryId = 'repo-456' as GraphQLNodeId;
    commitInfo = {
      oid: 'abc123' as CommitOid,
      branch: 'main' as BranchName,
      baseOid: 'def456' as CommitOid,
      message: 'Test commit',
      author: 'Test Author',
      authoredAt: new Date('2024-01-01'),
    };

    validParams = {
      runId,
      projectKey,
      repositoryId,
      commitInfo,
    };
  });

  const createTestIssue = (id: string): IssueOccurrence => ({
    id,
    issueCode: 'TEST001',
    analyzerShortcode: 'test-analyzer' as AnalyzerShortcode,
    category: 'BUG_RISK',
    severity: 'MAJOR',
    message: 'Test issue',
    path: '/test/file.ts',
    line: 10,
    column: 5,
  });

  describe('create', () => {
    it('should create an analysis run with valid parameters', () => {
      const run = AnalysisRun.create(validParams);

      expect(run.runId).toBe(runId);
      expect(run.projectKey).toBe(projectKey);
      expect(run.repositoryId).toBe(repositoryId);
      expect(run.commitInfo).toEqual(commitInfo);
      expect(run.status).toBe('PENDING');
      expect(run.issues).toHaveLength(0);
      expect(run.domainEvents).toHaveLength(1);
      expect(run.domainEvents[0].eventType).toBe('AnalysisRunCreated');
    });

    it('should create empty summary', () => {
      const run = AnalysisRun.create(validParams);
      const summary = run.summary;

      expect(summary.totalIntroduced.count).toBe(0);
      expect(summary.totalResolved.count).toBe(0);
      expect(summary.totalSuppressed.count).toBe(0);
      expect(summary.byAnalyzer).toHaveLength(0);
      expect(summary.byCategory).toHaveLength(0);
    });

    it('should set createdAt timestamp', () => {
      const before = new Date();
      const run = AnalysisRun.create(validParams);
      const after = new Date();

      const timestamps = run.timestamps;
      expect(timestamps.createdAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
      expect(timestamps.createdAt.getTime()).toBeLessThanOrEqual(after.getTime());
      expect(timestamps.startedAt).toBeUndefined();
      expect(timestamps.finishedAt).toBeUndefined();
    });

    it('should create run without optional commit info', () => {
      const minimalCommitInfo: CommitInfo = {
        oid: 'abc123' as CommitOid,
        branch: 'main' as BranchName,
        baseOid: 'def456' as CommitOid,
      };

      const params = {
        ...validParams,
        commitInfo: minimalCommitInfo,
      };

      const run = AnalysisRun.create(params);
      expect(run.commitInfo.message).toBeUndefined();
      expect(run.commitInfo.author).toBeUndefined();
      expect(run.commitInfo.authoredAt).toBeUndefined();
    });

    it('should emit AnalysisRunCreated event with correct payload', () => {
      const run = AnalysisRun.create(validParams);

      const events = run.domainEvents;
      expect(events).toHaveLength(1);

      const event = events[0];
      expect(event.eventType).toBe('AnalysisRunCreated');
      expect(event.aggregateId).toBe(runId);
      expect(event.payload).toEqual({
        projectKey,
        commitOid: commitInfo.oid,
        branch: commitInfo.branch,
      });
    });
  });

  describe('fromPersistence', () => {
    it('should recreate run from persistence without events', () => {
      const issues = [createTestIssue('issue-1'), createTestIssue('issue-2')];

      const summary: RunSummary = {
        totalIntroduced: IssueCount.create(2),
        totalResolved: IssueCount.create(0),
        totalSuppressed: IssueCount.create(0),
        byAnalyzer: [
          {
            analyzerShortcode: 'test-analyzer' as AnalyzerShortcode,
            introduced: IssueCount.create(2),
            resolved: IssueCount.create(0),
            suppressed: IssueCount.create(0),
          },
        ],
        byCategory: [
          {
            category: 'BUG_RISK',
            introduced: IssueCount.create(2),
            resolved: IssueCount.create(0),
            suppressed: IssueCount.create(0),
          },
        ],
      };

      const timestamps: RunTimestamps = {
        createdAt: new Date('2024-01-01'),
        startedAt: new Date('2024-01-01T00:01:00Z'),
        finishedAt: new Date('2024-01-01T00:05:00Z'),
      };

      const persistenceData = {
        runId,
        projectKey,
        repositoryId,
        commitInfo,
        status: 'SUCCESS' as const,
        timestamps,
        issues,
        summary,
      };

      const run = AnalysisRun.fromPersistence(persistenceData);

      expect(run.runId).toBe(runId);
      expect(run.projectKey).toBe(projectKey);
      expect(run.status).toBe('SUCCESS');
      expect(run.issues).toHaveLength(2);
      expect(run.domainEvents).toHaveLength(0); // No events when loading
      expect(run.timestamps).toEqual(timestamps);
      expect(run.summary.totalIntroduced.count).toBe(2);
    });
  });

  describe('status transitions', () => {
    describe('start', () => {
      it('should transition from PENDING to RUNNING', () => {
        const run = AnalysisRun.create(validParams);
        run.clearEvents();

        const beforeStart = run.timestamps.startedAt;
        expect(beforeStart).toBeUndefined();

        run.start();

        expect(run.status).toBe('RUNNING');
        expect(run.timestamps.startedAt).toBeDefined();
        expect(run.domainEvents).toHaveLength(2); // AnalysisRunStarted + AggregateModified
        expect(run.domainEvents[0].eventType).toBe('AnalysisRunStarted');
      });

      it('should throw error when starting from invalid status', () => {
        const run = AnalysisRun.create(validParams);
        run.start();

        // Try to start again
        expect(() => run.start()).toThrow('Invalid status transition from RUNNING to RUNNING');
      });
    });

    describe('complete', () => {
      it('should transition from RUNNING to SUCCESS', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.clearEvents();

        run.complete();

        expect(run.status).toBe('SUCCESS');
        expect(run.timestamps.finishedAt).toBeDefined();
        expect(run.isFinished).toBe(true);
        expect(run.canModify).toBe(false);
        expect(run.domainEvents[0].eventType).toBe('AnalysisRunCompleted');
        expect(run.domainEvents[0].payload).toMatchObject({
          summary: expect.objectContaining({
            totalIntroduced: expect.any(Object),
            totalResolved: expect.any(Object),
            totalSuppressed: expect.any(Object),
          }),
        });
      });

      it('should throw error when completing from invalid status', () => {
        const run = AnalysisRun.create(validParams);

        // Try to complete without starting
        expect(() => run.complete()).toThrow('Invalid status transition from PENDING to SUCCESS');
      });
    });

    describe('fail', () => {
      it('should transition from RUNNING to FAILURE with reason', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.clearEvents();

        const reason = 'Analysis engine crashed';
        run.fail(reason);

        expect(run.status).toBe('FAILURE');
        expect(run.timestamps.finishedAt).toBeDefined();
        expect(run.isFinished).toBe(true);
        expect(run.domainEvents[0].eventType).toBe('AnalysisRunFailed');
        expect(run.domainEvents[0].payload).toEqual({ reason });
      });
    });

    describe('timeout', () => {
      it('should transition from RUNNING to TIMEOUT', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.clearEvents();

        run.timeout();

        expect(run.status).toBe('TIMEOUT');
        expect(run.timestamps.finishedAt).toBeDefined();
        expect(run.isFinished).toBe(true);
        expect(run.domainEvents[0].eventType).toBe('AnalysisRunTimedOut');
      });
    });

    describe('cancel', () => {
      it('should transition from PENDING to CANCELLED', () => {
        const run = AnalysisRun.create(validParams);
        run.clearEvents();

        const reason = 'User requested cancellation';
        run.cancel(reason);

        expect(run.status).toBe('CANCELLED');
        expect(run.timestamps.finishedAt).toBeDefined();
        expect(run.isFinished).toBe(true);
        expect(run.domainEvents[0].eventType).toBe('AnalysisRunCancelled');
        expect(run.domainEvents[0].payload).toEqual({ reason });
      });

      it('should transition from RUNNING to CANCELLED', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.clearEvents();

        const reason = 'Timeout exceeded';
        run.cancel(reason);

        expect(run.status).toBe('CANCELLED');
      });
    });

    describe('skip', () => {
      it('should transition from PENDING to SKIPPED', () => {
        const run = AnalysisRun.create(validParams);
        run.clearEvents();

        const reason = 'No changes detected';
        run.skip(reason);

        expect(run.status).toBe('SKIPPED');
        expect(run.timestamps.finishedAt).toBeDefined();
        expect(run.isFinished).toBe(true);
        expect(run.domainEvents[0].eventType).toBe('AnalysisRunSkipped');
        expect(run.domainEvents[0].payload).toEqual({ reason });
      });
    });
  });

  describe('issue management', () => {
    describe('addIssue', () => {
      it('should add issue to running run', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.clearEvents();

        const issue = createTestIssue('issue-1');
        run.addIssue(issue);

        expect(run.issues).toHaveLength(1);
        expect(run.issues[0]).toEqual(issue);
        expect(run.domainEvents[0].eventType).toBe('IssueAdded');
        expect(run.domainEvents[0].payload).toEqual({
          issueId: issue.id,
          issueCode: issue.issueCode,
        });
      });

      it('should update summary when adding issues', () => {
        const run = AnalysisRun.create(validParams);
        run.start();

        const issue1 = createTestIssue('issue-1');
        const issue2 = {
          ...createTestIssue('issue-2'),
          analyzerShortcode: 'another-analyzer' as AnalyzerShortcode,
          category: 'SECURITY' as const,
        };

        run.addIssue(issue1);
        run.addIssue(issue2);

        const summary = run.summary;
        expect(summary.totalIntroduced.count).toBe(2);
        expect(summary.byAnalyzer).toHaveLength(2);
        expect(summary.byCategory).toHaveLength(2);

        const testAnalyzerDist = summary.byAnalyzer.find(
          (d) => d.analyzerShortcode === 'test-analyzer'
        );
        expect(testAnalyzerDist?.introduced.count).toBe(1);

        const securityDist = summary.byCategory.find((d) => d.category === 'SECURITY');
        expect(securityDist?.introduced.count).toBe(1);
      });

      it('should not add duplicate issues', () => {
        const run = AnalysisRun.create(validParams);
        run.start();

        const issue = createTestIssue('issue-1');
        run.addIssue(issue);
        run.clearEvents();

        // Try to add the same issue again
        run.addIssue(issue);

        expect(run.issues).toHaveLength(1);
        expect(run.domainEvents).toHaveLength(0); // No event for duplicate
      });

      it('should throw error when adding issue to finished run', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.complete();

        const issue = createTestIssue('issue-1');
        expect(() => run.addIssue(issue)).toThrow('Cannot add issues to a finished run');
      });
    });

    describe('removeIssue', () => {
      it('should remove existing issue', () => {
        const run = AnalysisRun.create(validParams);
        run.start();

        const issue = createTestIssue('issue-1');
        run.addIssue(issue);
        run.clearEvents();

        run.removeIssue(issue.id);

        expect(run.issues).toHaveLength(0);
        expect(run.domainEvents[0].eventType).toBe('IssueRemoved');
        expect(run.domainEvents[0].payload).toEqual({ issueId: issue.id });
      });

      it('should update summary when removing issues', () => {
        const run = AnalysisRun.create(validParams);
        run.start();

        const issue1 = createTestIssue('issue-1');
        const issue2 = createTestIssue('issue-2');
        run.addIssue(issue1);
        run.addIssue(issue2);

        run.removeIssue(issue1.id);

        const summary = run.summary;
        expect(summary.totalIntroduced.count).toBe(1);
      });

      it('should not emit event when removing non-existent issue', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.clearEvents();

        run.removeIssue('non-existent');

        expect(run.domainEvents).toHaveLength(0);
      });

      it('should throw error when removing issue from finished run', () => {
        const run = AnalysisRun.create(validParams);
        run.start();
        run.complete();

        expect(() => run.removeIssue('any-id')).toThrow('Cannot remove issues from a finished run');
      });
    });
  });

  describe('isFinished and canModify', () => {
    it('should identify finished statuses correctly', () => {
      const testCases = [
        { status: 'PENDING', isFinished: false, canModify: true },
        { status: 'RUNNING', isFinished: false, canModify: true },
        { status: 'SUCCESS', isFinished: true, canModify: false },
        { status: 'FAILURE', isFinished: true, canModify: false },
        { status: 'TIMEOUT', isFinished: true, canModify: false },
        { status: 'CANCELLED', isFinished: true, canModify: false },
        { status: 'SKIPPED', isFinished: true, canModify: false },
      ];

      testCases.forEach((testCase) => {
        const persistenceData = {
          runId,
          projectKey,
          repositoryId,
          commitInfo,
          status: testCase.status as RunStatus,
          timestamps: { createdAt: new Date() },
          issues: [],
          summary: {
            totalIntroduced: IssueCount.create(0),
            totalResolved: IssueCount.create(0),
            totalSuppressed: IssueCount.create(0),
            byAnalyzer: [],
            byCategory: [],
          },
        };

        const run = AnalysisRun.fromPersistence(persistenceData);
        expect(run.isFinished).toBe(testCase.isFinished);
        expect(run.canModify).toBe(testCase.canModify);
      });
    });
  });

  describe('toPersistence', () => {
    it('should convert to persistence format', () => {
      const run = AnalysisRun.create(validParams);
      run.start();

      const issue = createTestIssue('issue-1');
      run.addIssue(issue);

      const persistence = run.toPersistence();

      expect(persistence).toEqual({
        runId,
        projectKey,
        repositoryId,
        commitInfo,
        status: 'RUNNING',
        timestamps: expect.objectContaining({
          createdAt: expect.any(Date),
          startedAt: expect.any(Date),
        }),
        issues: [issue],
        summary: expect.objectContaining({
          totalIntroduced: expect.objectContaining({ count: 1 }),
          byAnalyzer: expect.arrayContaining([
            expect.objectContaining({ analyzerShortcode: 'test-analyzer' }),
          ]),
          byCategory: expect.arrayContaining([expect.objectContaining({ category: 'BUG_RISK' })]),
        }),
      });
    });

    it('should preserve all data through persistence round-trip', () => {
      const run = AnalysisRun.create(validParams);
      run.start();

      const issue1 = createTestIssue('issue-1');
      const issue2 = createTestIssue('issue-2');
      run.addIssue(issue1);
      run.addIssue(issue2);
      run.complete();

      const persistence = run.toPersistence();
      const reconstructed = AnalysisRun.fromPersistence(persistence);

      expect(reconstructed.runId).toBe(run.runId);
      expect(reconstructed.status).toBe(run.status);
      expect(reconstructed.issues).toHaveLength(2);
      expect(reconstructed.summary.totalIntroduced.count).toBe(2);
      expect(reconstructed.timestamps).toEqual(run.timestamps);
    });
  });

  describe('getters', () => {
    it('should return immutable commit info', () => {
      const run = AnalysisRun.create(validParams);
      const info = run.commitInfo;

      // Verify it's a copy
      expect(info).not.toBe(commitInfo);
      expect(info).toEqual(commitInfo);
    });

    it('should return immutable timestamps', () => {
      const run = AnalysisRun.create(validParams);
      const timestamps1 = run.timestamps;
      const timestamps2 = run.timestamps;

      // Should return new object each time
      expect(timestamps1).not.toBe(timestamps2);
      expect(timestamps1).toEqual(timestamps2);
    });

    it('should return issues as array', () => {
      const run = AnalysisRun.create(validParams);
      run.start();

      const issue1 = createTestIssue('issue-1');
      const issue2 = createTestIssue('issue-2');
      run.addIssue(issue1);
      run.addIssue(issue2);

      const issues = run.issues;
      expect(Array.isArray(issues)).toBe(true);
      expect(issues).toHaveLength(2);
      expect(issues).toContainEqual(issue1);
      expect(issues).toContainEqual(issue2);
    });

    it('should return immutable summary', () => {
      const run = AnalysisRun.create(validParams);
      const summary = run.summary;

      // Verify arrays are copies
      const originalAnalyzerLength = summary.byAnalyzer.length;
      summary.byAnalyzer.push({
        analyzerShortcode: 'test' as AnalyzerShortcode,
        introduced: IssueCount.create(0),
        resolved: IssueCount.create(0),
        suppressed: IssueCount.create(0),
      });
      expect(run.summary.byAnalyzer).toHaveLength(originalAnalyzerLength);
    });
  });

  describe('domain events', () => {
    it('should accumulate multiple events', () => {
      const run = AnalysisRun.create(validParams);
      run.clearEvents();

      run.start();
      run.addIssue(createTestIssue('issue-1'));
      run.complete();

      // Each operation emits 2 events (operation + AggregateModified)
      expect(run.domainEvents).toHaveLength(6);

      const eventTypes = run.domainEvents.map((e) => e.eventType);
      expect(eventTypes).toEqual([
        'AnalysisRunStarted',
        'AggregateModified',
        'IssueAdded',
        'AggregateModified',
        'AnalysisRunCompleted',
        'AggregateModified',
      ]);
    });

    it('should clear events when requested', () => {
      const run = AnalysisRun.create(validParams);

      expect(run.domainEvents).toHaveLength(1);

      run.clearEvents();

      expect(run.domainEvents).toHaveLength(0);
    });
  });

  describe('edge cases', () => {
    it('should handle empty issues correctly', () => {
      const run = AnalysisRun.create(validParams);
      run.start();
      run.complete();

      const summary = run.summary;
      expect(summary.totalIntroduced.count).toBe(0);
      expect(summary.byAnalyzer).toHaveLength(0);
      expect(summary.byCategory).toHaveLength(0);
    });

    it('should handle multiple issues from same analyzer', () => {
      const run = AnalysisRun.create(validParams);
      run.start();

      const analyzer = 'test-analyzer' as AnalyzerShortcode;
      run.addIssue({ ...createTestIssue('issue-1'), analyzerShortcode: analyzer });
      run.addIssue({ ...createTestIssue('issue-2'), analyzerShortcode: analyzer });
      run.addIssue({ ...createTestIssue('issue-3'), analyzerShortcode: analyzer });

      const summary = run.summary;
      const analyzerDist = summary.byAnalyzer.find((d) => d.analyzerShortcode === analyzer);
      expect(analyzerDist?.introduced.count).toBe(3);
    });

    it('should handle multiple issues from same category', () => {
      const run = AnalysisRun.create(validParams);
      run.start();

      const category = 'SECURITY' as const;
      run.addIssue({ ...createTestIssue('issue-1'), category });
      run.addIssue({ ...createTestIssue('issue-2'), category });

      const summary = run.summary;
      const categoryDist = summary.byCategory.find((d) => d.category === category);
      expect(categoryDist?.introduced.count).toBe(2);
    });

    it('should handle all issue categories', () => {
      const run = AnalysisRun.create(validParams);
      run.start();

      const categories = [
        'BUG_RISK',
        'SECURITY',
        'STYLE',
        'PERFORMANCE',
        'DOCUMENTATION',
        'COVERAGE',
        'COMPLEXITY',
      ] as const;

      categories.forEach((category, index) => {
        run.addIssue({ ...createTestIssue(`issue-${index}`), category });
      });

      const summary = run.summary;
      expect(summary.byCategory).toHaveLength(7);
      expect(summary.totalIntroduced.count).toBe(7);
    });
  });
});
