import { describe, it, expect, beforeEach, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mountUsersApi } from './users-api.js';
import type { Repository } from '../db/repository.js';

function makeRepo(overrides: Partial<Repository> = {}): Repository {
  return {
    listUserGiteaOrgs: vi.fn().mockReturnValue([]),
    listUserLocalOrgs: vi.fn().mockReturnValue([]),
    updateUser: vi.fn(),
    ...overrides,
  } as unknown as Repository;
}

function makeUser(overrides: Partial<Express.User> = {}): Express.User {
  return {
    id: 'user-1',
    email: 'u@example.com',
    name: 'User One',
    avatarUrl: null,
    role: 'user',
    status: 'active',
    orgIds: [],
    defaultVisibility: 'private',
    defaultVisibilityOrgId: null,
    ...overrides,
  };
}

/** Build an app with authActive=false and an optionally injected user. */
function makeApp(repo: Repository, user?: Express.User): express.Application {
  const app = express();
  if (user) {
    app.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = user;
      next();
    });
  }
  mountUsersApi(app, repo, false);
  return app;
}

describe('Users API', () => {
  let repo: Repository;

  beforeEach(() => {
    repo = makeRepo();
  });

  // -------------------------------------------------------------------
  // GET /api/users/me/orgs
  // -------------------------------------------------------------------

  describe('GET /api/users/me/orgs', () => {
    it('returns 401 when no user is injected (authActive=false defensive path)', async () => {
      const app = makeApp(repo);
      const res = await request(app).get('/api/users/me/orgs');
      expect(res.status).toBe(401);
      expect(res.body.error).toBe('Unauthorized');
      expect(repo.listUserGiteaOrgs).not.toHaveBeenCalled();
    });

    it('returns 401 via requireAuth when authActive=true and unauthenticated', async () => {
      const app = express();
      // Simulate passport being initialized but the session unauthenticated.
      app.use((req, _res, next) => {
        (req as unknown as { isAuthenticated: () => boolean }).isAuthenticated = () => false;
        next();
      });
      mountUsersApi(app, repo, true);
      const res = await request(app).get('/api/users/me/orgs');
      expect(res.status).toBe(401);
      expect(res.body.error).toBe('Unauthorized');
    });

    it('merges Gitea orgs and local orgs into one list', async () => {
      vi.mocked(repo.listUserGiteaOrgs).mockReturnValue([
        { orgId: 'g1', orgName: 'gitea-org', fetchedAt: '2026-01-01T00:00:00Z' },
      ] as never);
      vi.mocked(repo.listUserLocalOrgs).mockReturnValue([
        { orgId: 'l1', name: 'local-org' },
      ] as never);
      const app = makeApp(repo, makeUser());

      const res = await request(app).get('/api/users/me/orgs');

      expect(res.status).toBe(200);
      expect(res.body.orgs).toEqual([
        { orgId: 'g1', orgName: 'gitea-org', fetchedAt: '2026-01-01T00:00:00Z' },
        { orgId: 'l1', orgName: 'local-org', fetchedAt: '' },
      ]);
      expect(repo.listUserGiteaOrgs).toHaveBeenCalledWith('user-1');
      expect(repo.listUserLocalOrgs).toHaveBeenCalledWith('user-1');
    });

    it('returns an empty list when the user belongs to no orgs', async () => {
      const app = makeApp(repo, makeUser());
      const res = await request(app).get('/api/users/me/orgs');
      expect(res.status).toBe(200);
      expect(res.body.orgs).toEqual([]);
    });
  });

  // -------------------------------------------------------------------
  // PATCH /api/users/me/preferences
  // -------------------------------------------------------------------

  describe('PATCH /api/users/me/preferences', () => {
    it('returns 401 when no user is injected', async () => {
      const app = makeApp(repo);
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'public' });
      expect(res.status).toBe(401);
      expect(repo.updateUser).not.toHaveBeenCalled();
    });

    it('rejects an unknown defaultVisibility value with 400', async () => {
      const app = makeApp(repo, makeUser());
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'everyone' });
      expect(res.status).toBe(400);
      expect(res.body.error).toBe('invalid defaultVisibility');
      expect(repo.updateUser).not.toHaveBeenCalled();
    });

    it('rejects non-string defaultVisibility with 400', async () => {
      const app = makeApp(repo, makeUser());
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 42 });
      expect(res.status).toBe(400);
      expect(repo.updateUser).not.toHaveBeenCalled();
    });

    it('requires defaultVisibilityOrgId when defaultVisibility is "org"', async () => {
      const app = makeApp(repo, makeUser({ orgIds: ['org-a'] }));
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'org' });
      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/default_visibility_org_id is required/);
      expect(repo.updateUser).not.toHaveBeenCalled();
    });

    it('rejects an org scope the user does not belong to', async () => {
      const app = makeApp(repo, makeUser({ orgIds: ['org-a'] }));
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'org', defaultVisibilityOrgId: 'org-b' });
      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/must be one of your orgs/);
      expect(repo.updateUser).not.toHaveBeenCalled();
    });

    it('accepts defaultVisibility=org with an org the user belongs to', async () => {
      const app = makeApp(repo, makeUser({ orgIds: ['org-a', 'org-b'] }));
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'org', defaultVisibilityOrgId: 'org-b' });
      expect(res.status).toBe(200);
      expect(res.body).toEqual({ ok: true });
      expect(repo.updateUser).toHaveBeenCalledWith('user-1', {
        defaultVisibility: 'org',
        defaultVisibilityOrgId: 'org-b',
      });
    });

    it('accepts defaultVisibility=private and nulls the org scope', async () => {
      const app = makeApp(repo, makeUser());
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'private' });
      expect(res.status).toBe(200);
      expect(repo.updateUser).toHaveBeenCalledWith('user-1', {
        defaultVisibility: 'private',
        defaultVisibilityOrgId: null,
      });
    });

    it('accepts defaultVisibility=public', async () => {
      const app = makeApp(repo, makeUser());
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'public' });
      expect(res.status).toBe(200);
      expect(repo.updateUser).toHaveBeenCalledWith('user-1', {
        defaultVisibility: 'public',
        defaultVisibilityOrgId: null,
      });
    });

    it('treats an empty body as a no-op preference update (200)', async () => {
      const app = makeApp(repo, makeUser());
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({});
      expect(res.status).toBe(200);
      expect(repo.updateUser).toHaveBeenCalledWith('user-1', {
        defaultVisibility: undefined,
        defaultVisibilityOrgId: null,
      });
    });
  });
});
