import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import Fastify from 'fastify';
import type { FastifyInstance } from 'fastify';
import { registerAuthRoutes } from '../src/routes/auth.js';
import { errorHandler } from '../src/middleware/error-handler.js';
import type { AuthService, LoginResult } from '../src/services/auth.service.js';
import type { UserService } from '../src/services/user.service.js';
import type { GroupService } from '../src/services/group.service.js';
import type { RbacDefinitionService } from '../src/services/rbac-definition.service.js';
import type { RbacService, RbacAction } from '../src/services/rbac.service.js';
import type { SafeUser } from '../src/repositories/user.repository.js';
import type { RbacDefinition } from '@prisma/client';

let app: FastifyInstance;

afterEach(async () => {
  if (app) await app.close();
});

function makeLoginResult(overrides?: Partial<LoginResult>): LoginResult {
  return {
    token: 'test-token-123',
    expiresAt: new Date(Date.now() + 86400_000),
    user: { id: 'user-1', email: 'admin@example.com', role: 'user' },
    ...overrides,
  };
}

function makeSafeUser(overrides?: Partial<SafeUser>): SafeUser {
  return {
    id: 'user-1',
    email: 'admin@example.com',
    name: null,
    role: 'user',
    provider: 'local',
    externalId: null,
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function makeRbacDef(overrides?: Partial<RbacDefinition>): RbacDefinition {
  return {
    id: 'rbac-1',
    name: 'bootstrap-admin',
    subjects: [{ kind: 'Group', name: 'admin' }],
    roleBindings: [
      { role: 'edit', resource: '*' },
      { role: 'run', resource: '*' },
      { role: 'run', action: 'impersonate' },
      { role: 'run', action: 'logs' },
      { role: 'run', action: 'backup' },
      { role: 'run', action: 'restore' },
      { role: 'run', action: 'audit-purge' },
    ],
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

interface MockDeps {
  authService: {
    login: ReturnType<typeof vi.fn>;
    logout: ReturnType<typeof vi.fn>;
    findSession: ReturnType<typeof vi.fn>;
    impersonate: ReturnType<typeof vi.fn>;
  };
  userService: {
    count: ReturnType<typeof vi.fn>;
    create: ReturnType<typeof vi.fn>;
    list: ReturnType<typeof vi.fn>;
    getById: ReturnType<typeof vi.fn>;
    getByEmail: ReturnType<typeof vi.fn>;
    delete: ReturnType<typeof vi.fn>;
  };
  groupService: {
    create: ReturnType<typeof vi.fn>;
    list: ReturnType<typeof vi.fn>;
    getById: ReturnType<typeof vi.fn>;
    getByName: ReturnType<typeof vi.fn>;
    update: ReturnType<typeof vi.fn>;
    delete: ReturnType<typeof vi.fn>;
  };
  rbacDefinitionService: {
    create: ReturnType<typeof vi.fn>;
    list: ReturnType<typeof vi.fn>;
    getById: ReturnType<typeof vi.fn>;
    getByName: ReturnType<typeof vi.fn>;
    update: ReturnType<typeof vi.fn>;
    delete: ReturnType<typeof vi.fn>;
  };
  rbacService: {
    canAccess: ReturnType<typeof vi.fn>;
    canRunOperation: ReturnType<typeof vi.fn>;
    getPermissions: ReturnType<typeof vi.fn>;
  };
}

function createMockDeps(): MockDeps {
  return {
    authService: {
      login: vi.fn(async () => makeLoginResult()),
      logout: vi.fn(async () => {}),
      findSession: vi.fn(async () => null),
      impersonate: vi.fn(async () => makeLoginResult({ token: 'impersonated-token' })),
    },
    userService: {
      count: vi.fn(async () => 0),
      create: vi.fn(async () => makeSafeUser()),
      list: vi.fn(async () => []),
      getById: vi.fn(async () => makeSafeUser()),
      getByEmail: vi.fn(async () => makeSafeUser()),
      delete: vi.fn(async () => {}),
    },
    groupService: {
      create: vi.fn(async () => ({ id: 'grp-1', name: 'admin', description: 'Bootstrap admin group', members: [] })),
      list: vi.fn(async () => []),
      getById: vi.fn(async () => null),
      getByName: vi.fn(async () => null),
      update: vi.fn(async () => null),
      delete: vi.fn(async () => {}),
    },
    rbacDefinitionService: {
      create: vi.fn(async () => makeRbacDef()),
      list: vi.fn(async () => []),
      getById: vi.fn(async () => makeRbacDef()),
      getByName: vi.fn(async () => null),
      update: vi.fn(async () => makeRbacDef()),
      delete: vi.fn(async () => {}),
    },
    rbacService: {
      canAccess: vi.fn(async () => false),
      canRunOperation: vi.fn(async () => false),
      getPermissions: vi.fn(async () => []),
    },
  };
}

function createApp(deps: MockDeps): Promise<FastifyInstance> {
  app = Fastify({ logger: false });
  app.setErrorHandler(errorHandler);
  registerAuthRoutes(app, deps as unknown as {
    authService: AuthService;
    userService: UserService;
    groupService: GroupService;
    rbacDefinitionService: RbacDefinitionService;
    rbacService: RbacService;
  });
  return app.ready();
}

describe('Auth Bootstrap', () => {
  describe('GET /api/v1/auth/status', () => {
    it('returns hasUsers: false when no users exist', async () => {
      const deps = createMockDeps();
      deps.userService.count.mockResolvedValue(0);
      await createApp(deps);

      const res = await app.inject({ method: 'GET', url: '/api/v1/auth/status' });
      expect(res.statusCode).toBe(200);
      expect(res.json<{ hasUsers: boolean }>().hasUsers).toBe(false);
    });

    it('returns hasUsers: true when users exist', async () => {
      const deps = createMockDeps();
      deps.userService.count.mockResolvedValue(1);
      await createApp(deps);

      const res = await app.inject({ method: 'GET', url: '/api/v1/auth/status' });
      expect(res.statusCode).toBe(200);
      expect(res.json<{ hasUsers: boolean }>().hasUsers).toBe(true);
    });
  });

  describe('POST /api/v1/auth/bootstrap', () => {
    it('creates admin user, admin group, RBAC definition targeting group, and returns session token', async () => {
      const deps = createMockDeps();
      deps.userService.count.mockResolvedValue(0);
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/bootstrap',
        payload: { email: 'admin@example.com', password: 'securepass123' },
      });

      expect(res.statusCode).toBe(201);
      const body = res.json<LoginResult>();
      expect(body.token).toBe('test-token-123');
      expect(body.user.email).toBe('admin@example.com');

      // Verify user was created
      expect(deps.userService.create).toHaveBeenCalledWith({
        email: 'admin@example.com',
        password: 'securepass123',
      });

      // Verify admin group was created with the user as member
      expect(deps.groupService.create).toHaveBeenCalledWith({
        name: 'admin',
        description: 'Bootstrap admin group',
        members: ['admin@example.com'],
      });

      // Verify RBAC definition targets the Group, not the User
      expect(deps.rbacDefinitionService.create).toHaveBeenCalledWith({
        name: 'bootstrap-admin',
        subjects: [{ kind: 'Group', name: 'admin' }],
        roleBindings: [
          { role: 'edit', resource: '*' },
          { role: 'run', resource: '*' },
          { role: 'run', action: 'impersonate' },
          { role: 'run', action: 'logs' },
          { role: 'run', action: 'backup' },
          { role: 'run', action: 'restore' },
          { role: 'run', action: 'audit-purge' },
        ],
      });

      // Verify auto-login was called
      expect(deps.authService.login).toHaveBeenCalledWith('admin@example.com', 'securepass123');
    });

    it('passes name when provided', async () => {
      const deps = createMockDeps();
      deps.userService.count.mockResolvedValue(0);
      await createApp(deps);

      await app.inject({
        method: 'POST',
        url: '/api/v1/auth/bootstrap',
        payload: { email: 'admin@example.com', password: 'securepass123', name: 'Admin User' },
      });

      expect(deps.userService.create).toHaveBeenCalledWith({
        email: 'admin@example.com',
        password: 'securepass123',
        name: 'Admin User',
      });
    });

    it('returns 409 when users already exist', async () => {
      const deps = createMockDeps();
      deps.userService.count.mockResolvedValue(1);
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/bootstrap',
        payload: { email: 'admin@example.com', password: 'securepass123' },
      });

      expect(res.statusCode).toBe(409);
      expect(res.json<{ error: string }>().error).toContain('Users already exist');

      // Should NOT have created user, group, or RBAC
      expect(deps.userService.create).not.toHaveBeenCalled();
      expect(deps.groupService.create).not.toHaveBeenCalled();
      expect(deps.rbacDefinitionService.create).not.toHaveBeenCalled();
    });

    it('validates email and password via UserService', async () => {
      const deps = createMockDeps();
      deps.userService.count.mockResolvedValue(0);
      // Simulate Zod validation error from UserService
      deps.userService.create.mockRejectedValue(
        Object.assign(new Error('Validation error'), { statusCode: 400, issues: [] }),
      );
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/bootstrap',
        payload: { email: 'not-an-email', password: 'short' },
      });

      // The error handler should handle the validation error
      expect(res.statusCode).toBeGreaterThanOrEqual(400);
    });
  });

  describe('POST /api/v1/auth/login', () => {
    it('logs in successfully', async () => {
      const deps = createMockDeps();
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/login',
        payload: { email: 'admin@example.com', password: 'securepass123' },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json<LoginResult>().token).toBe('test-token-123');
    });
  });

  describe('POST /api/v1/auth/logout', () => {
    it('logs out with valid token', async () => {
      const deps = createMockDeps();
      deps.authService.findSession.mockResolvedValue({
        userId: 'user-1',
        expiresAt: new Date(Date.now() + 86400_000),
      });
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/logout',
        headers: { authorization: 'Bearer valid-token' },
      });

      expect(res.statusCode).toBe(200);
      expect(res.json<{ success: boolean }>().success).toBe(true);
      expect(deps.authService.logout).toHaveBeenCalledWith('valid-token');
    });

    it('returns 401 without auth', async () => {
      const deps = createMockDeps();
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/logout',
      });

      expect(res.statusCode).toBe(401);
    });
  });

  describe('POST /api/v1/auth/impersonate', () => {
    it('creates session for target user when caller is admin', async () => {
      const deps = createMockDeps();
      // Auth: valid session
      deps.authService.findSession.mockResolvedValue({
        userId: 'admin-user-id',
        expiresAt: new Date(Date.now() + 86400_000),
      });
      // RBAC: allow impersonate operation
      deps.rbacService.canRunOperation.mockResolvedValue(true);
      // Impersonate returns token for target
      deps.authService.impersonate.mockResolvedValue(
        makeLoginResult({ token: 'impersonated-token', user: { id: 'user-2', email: 'target@example.com', role: 'user' } }),
      );
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/impersonate',
        headers: { authorization: 'Bearer admin-token' },
        payload: { email: 'target@example.com' },
      });

      expect(res.statusCode).toBe(200);
      const body = res.json<LoginResult>();
      expect(body.token).toBe('impersonated-token');
      expect(body.user.email).toBe('target@example.com');
      expect(deps.authService.impersonate).toHaveBeenCalledWith('target@example.com');
    });

    it('returns 401 without auth', async () => {
      const deps = createMockDeps();
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/impersonate',
        payload: { email: 'target@example.com' },
      });

      expect(res.statusCode).toBe(401);
    });

    it('returns 403 when caller lacks admin permission on users', async () => {
      const deps = createMockDeps();
      // Auth: valid session
      deps.authService.findSession.mockResolvedValue({
        userId: 'non-admin-id',
        expiresAt: new Date(Date.now() + 86400_000),
      });
      // RBAC: deny
      deps.rbacService.canRunOperation.mockResolvedValue(false);
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/impersonate',
        headers: { authorization: 'Bearer regular-token' },
        payload: { email: 'target@example.com' },
      });

      expect(res.statusCode).toBe(403);
    });

    it('returns 401 when impersonation target does not exist', async () => {
      const deps = createMockDeps();
      // Auth: valid session
      deps.authService.findSession.mockResolvedValue({
        userId: 'admin-user-id',
        expiresAt: new Date(Date.now() + 86400_000),
      });
      // RBAC: allow
      deps.rbacService.canRunOperation.mockResolvedValue(true);
      // Impersonate fails — user not found
      const authError = new Error('User not found');
      (authError as Error & { statusCode: number }).statusCode = 401;
      deps.authService.impersonate.mockRejectedValue(authError);
      await createApp(deps);

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/auth/impersonate',
        headers: { authorization: 'Bearer admin-token' },
        payload: { email: 'nonexistent@example.com' },
      });

      expect(res.statusCode).toBe(401);
    });
  });
});
