/**
 * Unit tests for src/services/OrgService.ts
 */

import { OrgService } from '../../../src/services/OrgService';
import { OrgRepository } from '../../../src/repositories/OrgRepository';
import { AgentRepository } from '../../../src/repositories/AgentRepository';
import {
  OrgNotFoundError,
  OrgHasActiveAgentsError,
  AlreadyMemberError,
  ValidationError,
  AgentNotFoundError,
} from '../../../src/utils/errors';
import { IOrganization, IOrgMember } from '../../../src/types/organization';
import { IAgent } from '../../../src/types/index';

// Mock dependencies
jest.mock('../../../src/repositories/OrgRepository');
jest.mock('../../../src/repositories/AgentRepository');

const MockOrgRepository = OrgRepository as jest.MockedClass<typeof OrgRepository>;
const MockAgentRepository = AgentRepository as jest.MockedClass<typeof AgentRepository>;

const MOCK_ORG: IOrganization = {
  organizationId: 'org_01ABCDEFGHIJKLMNOPQRSTU',
  name: 'Acme Corp',
  slug: 'acme-corp',
  planTier: 'pro',
  maxAgents: 100,
  maxTokensPerMonth: 10000,
  status: 'active',
  createdAt: new Date('2026-03-28T09:00:00Z'),
  updatedAt: new Date('2026-03-28T09:00:00Z'),
};

const MOCK_AGENT: IAgent = {
  agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  organizationId: MOCK_ORG.organizationId,
  email: 'agent@sentryagent.ai',
  agentType: 'screener',
  version: '1.0.0',
  capabilities: ['resume:read'],
  owner: 'team-a',
  deploymentEnv: 'production',
  status: 'active',
  isPublic: false,
  createdAt: new Date('2026-03-28T09:00:00Z'),
  updatedAt: new Date('2026-03-28T09:00:00Z'),
};

const MOCK_MEMBER: IOrgMember = {
  memberId: 'mem_01ABCDEFGHIJKLMNOPQRSTU',
  organizationId: MOCK_ORG.organizationId,
  agentId: MOCK_AGENT.agentId,
  role: 'member',
  joinedAt: new Date('2026-03-28T09:00:00Z'),
};

describe('OrgService', () => {
  let orgService: OrgService;
  let orgRepo: jest.Mocked<OrgRepository>;
  let agentRepo: jest.Mocked<AgentRepository>;

  beforeEach(() => {
    jest.clearAllMocks();
    orgRepo = new MockOrgRepository({} as never) as jest.Mocked<OrgRepository>;
    agentRepo = new MockAgentRepository({} as never) as jest.Mocked<AgentRepository>;
    orgService = new OrgService(orgRepo, agentRepo);
  });

  // ────────────────────────────────────────────────────────────────
  // createOrg
  // ────────────────────────────────────────────────────────────────
  describe('createOrg()', () => {
    const createData = { name: 'Acme Corp', slug: 'acme-corp', planTier: 'pro' as const };

    it('should create and return a new organization when slug is unique', async () => {
      orgRepo.findBySlug.mockResolvedValue(null);
      orgRepo.create.mockResolvedValue(MOCK_ORG);

      const result = await orgService.createOrg(createData);

      expect(result).toEqual(MOCK_ORG);
      expect(orgRepo.findBySlug).toHaveBeenCalledWith(createData.slug);
      expect(orgRepo.create).toHaveBeenCalledWith(createData);
    });

    it('should throw ValidationError with SLUG_ALREADY_EXISTS when slug is taken', async () => {
      orgRepo.findBySlug.mockResolvedValue(MOCK_ORG);

      await expect(orgService.createOrg(createData)).rejects.toThrow(ValidationError);
      await expect(orgService.createOrg(createData)).rejects.toMatchObject({
        details: { code: 'SLUG_ALREADY_EXISTS', slug: createData.slug },
      });
      expect(orgRepo.create).not.toHaveBeenCalled();
    });
  });

  // ────────────────────────────────────────────────────────────────
  // listOrgs
  // ────────────────────────────────────────────────────────────────
  describe('listOrgs()', () => {
    it('should return a paginated list of organizations', async () => {
      orgRepo.findAll.mockResolvedValue({ orgs: [MOCK_ORG], total: 1 });

      const result = await orgService.listOrgs({ page: 1, limit: 20 });

      expect(result.data).toHaveLength(1);
      expect(result.total).toBe(1);
      expect(result.page).toBe(1);
      expect(result.limit).toBe(20);
    });

    it('should pass filters through to the repository', async () => {
      orgRepo.findAll.mockResolvedValue({ orgs: [], total: 0 });

      await orgService.listOrgs({ page: 2, limit: 10, status: 'active' });

      expect(orgRepo.findAll).toHaveBeenCalledWith({ page: 2, limit: 10, status: 'active' });
    });

    it('should return an empty list when no organizations exist', async () => {
      orgRepo.findAll.mockResolvedValue({ orgs: [], total: 0 });

      const result = await orgService.listOrgs({ page: 1, limit: 20 });

      expect(result.data).toHaveLength(0);
      expect(result.total).toBe(0);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // getOrg
  // ────────────────────────────────────────────────────────────────
  describe('getOrg()', () => {
    it('should return the organization when found', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);

      const result = await orgService.getOrg(MOCK_ORG.organizationId);

      expect(result).toEqual(MOCK_ORG);
      expect(orgRepo.findById).toHaveBeenCalledWith(MOCK_ORG.organizationId);
    });

    it('should throw OrgNotFoundError when organization does not exist', async () => {
      orgRepo.findById.mockResolvedValue(null);

      await expect(orgService.getOrg('nonexistent-org-id')).rejects.toThrow(OrgNotFoundError);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // updateOrg
  // ────────────────────────────────────────────────────────────────
  describe('updateOrg()', () => {
    it('should update and return the organization', async () => {
      const updated = { ...MOCK_ORG, name: 'Acme Corp Updated' };
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      orgRepo.update.mockResolvedValue(updated);

      const result = await orgService.updateOrg(MOCK_ORG.organizationId, { name: 'Acme Corp Updated' });

      expect(result.name).toBe('Acme Corp Updated');
      expect(orgRepo.update).toHaveBeenCalledWith(MOCK_ORG.organizationId, { name: 'Acme Corp Updated' });
    });

    it('should throw OrgNotFoundError when organization does not exist on findById', async () => {
      orgRepo.findById.mockResolvedValue(null);

      await expect(
        orgService.updateOrg('nonexistent-id', { name: 'New Name' }),
      ).rejects.toThrow(OrgNotFoundError);
      expect(orgRepo.update).not.toHaveBeenCalled();
    });

    it('should throw OrgNotFoundError when update returns null (race condition)', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      orgRepo.update.mockResolvedValue(null);

      await expect(
        orgService.updateOrg(MOCK_ORG.organizationId, { name: 'New Name' }),
      ).rejects.toThrow(OrgNotFoundError);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // deleteOrg
  // ────────────────────────────────────────────────────────────────
  describe('deleteOrg()', () => {
    it('should soft-delete the organization when it exists and has no active agents', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      orgRepo.countActiveAgents.mockResolvedValue(0);
      orgRepo.softDelete.mockResolvedValue(true);

      await orgService.deleteOrg(MOCK_ORG.organizationId);

      expect(orgRepo.softDelete).toHaveBeenCalledWith(MOCK_ORG.organizationId);
    });

    it('should throw OrgNotFoundError when organization does not exist', async () => {
      orgRepo.findById.mockResolvedValue(null);

      await expect(orgService.deleteOrg('nonexistent-id')).rejects.toThrow(OrgNotFoundError);
      expect(orgRepo.countActiveAgents).not.toHaveBeenCalled();
      expect(orgRepo.softDelete).not.toHaveBeenCalled();
    });

    it('should throw OrgHasActiveAgentsError when organization has active agents', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      orgRepo.countActiveAgents.mockResolvedValue(3);

      await expect(orgService.deleteOrg(MOCK_ORG.organizationId)).rejects.toThrow(OrgHasActiveAgentsError);
      expect(orgRepo.softDelete).not.toHaveBeenCalled();
    });

    it('should include activeCount in OrgHasActiveAgentsError details', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      orgRepo.countActiveAgents.mockResolvedValue(5);

      await expect(orgService.deleteOrg(MOCK_ORG.organizationId)).rejects.toMatchObject({
        details: { activeCount: 5 },
      });
    });
  });

  // ────────────────────────────────────────────────────────────────
  // addMember
  // ────────────────────────────────────────────────────────────────
  describe('addMember()', () => {
    const addMemberData = { agentId: MOCK_AGENT.agentId, role: 'member' as const };

    it('should add the agent as a member and return the membership record', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      orgRepo.findMember.mockResolvedValue(null);
      orgRepo.addMember.mockResolvedValue(MOCK_MEMBER);

      const result = await orgService.addMember(MOCK_ORG.organizationId, addMemberData);

      expect(result).toEqual(MOCK_MEMBER);
      expect(orgRepo.addMember).toHaveBeenCalledWith(
        MOCK_ORG.organizationId,
        MOCK_AGENT.agentId,
        'member',
      );
    });

    it('should throw OrgNotFoundError when organization does not exist', async () => {
      orgRepo.findById.mockResolvedValue(null);

      await expect(
        orgService.addMember('nonexistent-org', addMemberData),
      ).rejects.toThrow(OrgNotFoundError);
      expect(agentRepo.findById).not.toHaveBeenCalled();
    });

    it('should throw AgentNotFoundError when agent does not exist', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      agentRepo.findById.mockResolvedValue(null);

      await expect(
        orgService.addMember(MOCK_ORG.organizationId, addMemberData),
      ).rejects.toThrow(AgentNotFoundError);
      expect(orgRepo.findMember).not.toHaveBeenCalled();
    });

    it('should throw AlreadyMemberError when agent is already a member', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      orgRepo.findMember.mockResolvedValue(MOCK_MEMBER);

      await expect(
        orgService.addMember(MOCK_ORG.organizationId, addMemberData),
      ).rejects.toThrow(AlreadyMemberError);
      expect(orgRepo.addMember).not.toHaveBeenCalled();
    });

    it('should add member with admin role when role is admin', async () => {
      orgRepo.findById.mockResolvedValue(MOCK_ORG);
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      orgRepo.findMember.mockResolvedValue(null);
      orgRepo.addMember.mockResolvedValue({ ...MOCK_MEMBER, role: 'admin' });

      const result = await orgService.addMember(MOCK_ORG.organizationId, {
        agentId: MOCK_AGENT.agentId,
        role: 'admin',
      });

      expect(result.role).toBe('admin');
      expect(orgRepo.addMember).toHaveBeenCalledWith(
        MOCK_ORG.organizationId,
        MOCK_AGENT.agentId,
        'admin',
      );
    });
  });
});
