import { AdminVoteSession } from './AdminVoteSession';
import { IdentityDomainValidationError, IdentityDomainInvariantError } from '../errors/IdentityDomainError';

describe('AdminVoteSession', () => {
  const now = new Date('2025-01-01T00:00:00Z');
  const tomorrow = new Date('2025-01-02T00:00:00Z');
  const dayAfter = new Date('2025-01-03T00:00:00Z');

  describe('create', () => {
    it('should create a valid vote session', () => {
      const session = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2', 'user-3'],
      });

      expect(session.id).toBe('vote-123');
      expect(session.leagueId).toBe('league-456');
      expect(session.adminId).toBe('admin-789');
      expect(session.startDate).toEqual(now);
      expect(session.endDate).toEqual(tomorrow);
      expect(session.eligibleVoters).toEqual(['user-1', 'user-2', 'user-3']);
      expect(session.votes).toEqual([]);
      expect(session.closed).toBe(false);
      expect(session.outcome).toBeUndefined();
    });

    it('should throw error for missing voteSessionId', () => {
      expect(() => AdminVoteSession.create({
        voteSessionId: '',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1'],
      })).toThrow(IdentityDomainValidationError);
    });

    it('should throw error for missing leagueId', () => {
      expect(() => AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: '',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1'],
      })).toThrow(IdentityDomainValidationError);
    });

    it('should throw error for missing adminId', () => {
      expect(() => AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: '',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1'],
      })).toThrow(IdentityDomainValidationError);
    });

    it('should throw error for invalid date range', () => {
      expect(() => AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: tomorrow,
        endDate: now, // End before start
        eligibleVoters: ['user-1'],
      })).toThrow(IdentityDomainInvariantError);
    });

    it('should throw error for empty eligible voters', () => {
      expect(() => AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: [],
      })).toThrow(IdentityDomainValidationError);
    });

    it('should throw error for duplicate eligible voters', () => {
      expect(() => AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2', 'user-1'],
      })).toThrow(IdentityDomainInvariantError);
    });

    it('should accept optional votes and outcome', () => {
      const session = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2'],
        votes: [
          { voterId: 'user-1', positive: true, votedAt: now },
        ],
        closed: true,
        outcome: {
          percentPositive: 100,
          count: { positive: 1, negative: 0, total: 1 },
          eligibleVoterCount: 2,
          participationRate: 50,
          outcome: 'positive',
        },
      });

      expect(session.votes.length).toBe(1);
      expect(session.closed).toBe(true);
      expect(session.outcome).toBeDefined();
    });
  });

  describe('rehydrate', () => {
    it('should rehydrate from persisted data', () => {
      const session = AdminVoteSession.rehydrate({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2'],
        votes: [{ voterId: 'user-1', positive: true, votedAt: now }],
        closed: true,
        outcome: {
          percentPositive: 100,
          count: { positive: 1, negative: 0, total: 1 },
          eligibleVoterCount: 2,
          participationRate: 50,
          outcome: 'positive',
        },
        createdAt: now,
        updatedAt: tomorrow,
      });

      expect(session.id).toBe('vote-123');
      expect(session.closed).toBe(true);
      expect(session.votes.length).toBe(1);
    });
  });

  describe('castVote', () => {
    let session: AdminVoteSession;

    beforeEach(() => {
      session = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2', 'user-3'],
      });
    });

    it('should allow eligible voter to cast positive vote', () => {
      session.castVote('user-1', true, now);

      expect(session.votes.length).toBe(1);
      const vote = session.votes[0];
      expect(vote).toBeDefined();
      expect(vote!.voterId).toBe('user-1');
      expect(vote!.positive).toBe(true);
      expect(vote!.votedAt).toEqual(now);
    });

    it('should allow eligible voter to cast negative vote', () => {
      session.castVote('user-1', false, now);

      expect(session.votes.length).toBe(1);
      const vote = session.votes[0];
      expect(vote).toBeDefined();
      expect(vote!.positive).toBe(false);
    });

    it('should throw error if voter is not eligible', () => {
      expect(() => session.castVote('user-999', true, now))
        .toThrow(IdentityDomainInvariantError);
    });

    it('should throw error if voter already voted', () => {
      session.castVote('user-1', true, now);
      
      expect(() => session.castVote('user-1', false, now))
        .toThrow(IdentityDomainInvariantError);
    });

    it('should throw error if session is closed', () => {
      session.close();
      
      expect(() => session.castVote('user-1', true, now))
        .toThrow(IdentityDomainInvariantError);
    });

    it('should throw error if vote is outside voting window', () => {
      const beforeStart = new Date('2024-12-31T23:59:59Z');
      const afterEnd = new Date('2025-01-02T00:00:01Z');

      expect(() => session.castVote('user-1', true, beforeStart))
        .toThrow(IdentityDomainInvariantError);
      expect(() => session.castVote('user-1', true, afterEnd))
        .toThrow(IdentityDomainInvariantError);
    });

    it('should update updatedAt timestamp', () => {
      const originalUpdatedAt = session.updatedAt;
      const voteTime = new Date(now.getTime() + 1000);
      
      session.castVote('user-1', true, voteTime);
      
      expect(session.updatedAt).not.toEqual(originalUpdatedAt);
    });
  });

  describe('close', () => {
    let session: AdminVoteSession;

    beforeEach(() => {
      session = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2', 'user-3', 'user-4'],
      });
    });

    it('should close session and calculate positive outcome', () => {
      session.castVote('user-1', true, now);
      session.castVote('user-2', true, now);
      session.castVote('user-3', false, now);

      const outcome = session.close();

      expect(session.closed).toBe(true);
      expect(outcome).toEqual({
        percentPositive: 66.67,
        count: { positive: 2, negative: 1, total: 3 },
        eligibleVoterCount: 4,
        participationRate: 75,
        outcome: 'positive',
      });
    });

    it('should calculate negative outcome', () => {
      session.castVote('user-1', false, now);
      session.castVote('user-2', false, now);
      session.castVote('user-3', true, now);

      const outcome = session.close();

      expect(outcome.outcome).toBe('negative');
      expect(outcome.percentPositive).toBe(33.33);
    });

    it('should calculate tie outcome', () => {
      session.castVote('user-1', true, now);
      session.castVote('user-2', false, now);

      const outcome = session.close();

      expect(outcome.outcome).toBe('tie');
      expect(outcome.percentPositive).toBe(50);
    });

    it('should handle no votes', () => {
      const outcome = session.close();

      expect(outcome).toEqual({
        percentPositive: 0,
        count: { positive: 0, negative: 0, total: 0 },
        eligibleVoterCount: 4,
        participationRate: 0,
        outcome: 'tie',
      });
    });

    it('should throw error if already closed', () => {
      session.close();
      
      expect(() => session.close()).toThrow(IdentityDomainInvariantError);
    });

    it('should throw error if closed outside voting window', () => {
      const pastSession = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: new Date('2024-01-01'),
        endDate: new Date('2024-01-02'),
        eligibleVoters: ['user-1'],
      });

      expect(() => pastSession.close()).toThrow(IdentityDomainInvariantError);
    });

    it('should round percentPositive to 2 decimal places', () => {
      session.castVote('user-1', true, now);
      session.castVote('user-2', true, now);
      session.castVote('user-3', true, now);
      session.castVote('user-4', false, now);

      const outcome = session.close();

      expect(outcome.percentPositive).toBe(75.00);
    });
  });

  describe('helper methods', () => {
    let session: AdminVoteSession;

    beforeEach(() => {
      session = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2'],
      });
    });

    describe('hasVoted', () => {
      it('should return true if voter has voted', () => {
        session.castVote('user-1', true, now);
        expect(session.hasVoted('user-1')).toBe(true);
      });

      it('should return false if voter has not voted', () => {
        expect(session.hasVoted('user-1')).toBe(false);
      });
    });

    describe('getVote', () => {
      it('should return vote if exists', () => {
        session.castVote('user-1', true, now);
        const vote = session.getVote('user-1');
        
        expect(vote).toBeDefined();
        expect(vote?.voterId).toBe('user-1');
        expect(vote?.positive).toBe(true);
      });

      it('should return undefined if vote does not exist', () => {
        expect(session.getVote('user-1')).toBeUndefined();
      });
    });

    describe('getVoteCount', () => {
      it('should return correct count', () => {
        expect(session.getVoteCount()).toBe(0);
        
        session.castVote('user-1', true, now);
        expect(session.getVoteCount()).toBe(1);
        
        session.castVote('user-2', false, now);
        expect(session.getVoteCount()).toBe(2);
      });
    });

    describe('isVotingWindowOpen', () => {
      it('should return true during voting window', () => {
        expect(session.isVotingWindowOpen(now)).toBe(true);
        
        const midPoint = new Date((now.getTime() + tomorrow.getTime()) / 2);
        expect(session.isVotingWindowOpen(midPoint)).toBe(true);
      });

      it('should return false before voting window', () => {
        const before = new Date('2024-12-31T23:59:59Z');
        expect(session.isVotingWindowOpen(before)).toBe(false);
      });

      it('should return false after voting window', () => {
        const after = new Date('2025-01-02T00:00:01Z');
        expect(session.isVotingWindowOpen(after)).toBe(false);
      });

      it('should return false if session is closed', () => {
        session.close();
        expect(session.isVotingWindowOpen(now)).toBe(false);
      });
    });
  });

  describe('toJSON', () => {
    it('should serialize to JSON correctly', () => {
      const session = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1', 'user-2'],
      });

      session.castVote('user-1', true, now);
      session.close();

      const json = session.toJSON();

      expect(json.voteSessionId).toBe('vote-123');
      expect(json.leagueId).toBe('league-456');
      expect(json.adminId).toBe('admin-789');
      expect(json.closed).toBe(true);
      expect(json.votes).toHaveLength(1);
      expect(json.outcome).toBeDefined();
    });
  });

  describe('equals', () => {
    it('should return true for same ID', () => {
      const session1 = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1'],
      });

      const session2 = AdminVoteSession.rehydrate({
        voteSessionId: 'vote-123',
        leagueId: 'league-789', // Different
        adminId: 'admin-999', // Different
        startDate: tomorrow, // Different
        endDate: dayAfter, // Different
        eligibleVoters: ['user-999'], // Different
      });

      expect(session1.equals(session2)).toBe(true);
    });

    it('should return false for different IDs', () => {
      const session1 = AdminVoteSession.create({
        voteSessionId: 'vote-123',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1'],
      });

      const session2 = AdminVoteSession.create({
        voteSessionId: 'vote-456',
        leagueId: 'league-456',
        adminId: 'admin-789',
        startDate: now,
        endDate: tomorrow,
        eligibleVoters: ['user-1'],
      });

      expect(session1.equals(session2)).toBe(false);
    });
  });
});
