import { describe, expect, it, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import express, { Request, Response, NextFunction } from 'express';
import request from 'supertest';
import { Repository } from '../db/repository.js';
import { mountUsersApi } from './users-api.js';

describe('GET /api/jobs/:id visibility', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });

  it('non-viewer gets null from repo.getJob (drives 404 in handler)', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-vis-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const job = await repo.createJob({
        repo: 'local/task-1',
        issueNumber: 1,
        instruction: 'x',
        pieceName: 'chat',
        ownerId: alice.id,
        visibility: 'private',
        visibilityScopeOrgId: null,
      });

      const bobUser: Express.User = {
        id: 'bob-id', email: 'b@x.com', name: 'b', avatarUrl: null,
        role: 'user', status: 'active',
        orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
      };

      const aliceUser: Express.User = {
        ...alice,
        orgIds: [],
        defaultVisibility: 'private' as const,
        defaultVisibilityOrgId: null,
      };

      // Verify at the data layer: bob (non-owner, no orgs) cannot see alice's private job.
      expect(await repo.getJob(job.id, { viewer: bobUser })).toBeNull();
      // Alice (owner) can.
      expect(await repo.getJob(job.id, { viewer: aliceUser })).not.toBeNull();
      // Internal callers (no viewer) still get the row (worker/scheduler pass-through).
      expect(await repo.getJob(job.id)).not.toBeNull();
    } finally {
      repo.close();
    }
  });

  it('admin sees any job regardless of visibility', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-vis-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const job = await repo.createJob({
        repo: 'local/task-1',
        issueNumber: 1,
        instruction: 'x',
        pieceName: 'chat',
        ownerId: alice.id,
        visibility: 'private',
        visibilityScopeOrgId: null,
      });

      const adminUser: Express.User = {
        id: 'admin-id', email: 'admin@x.com', name: 'admin', avatarUrl: null,
        role: 'admin', status: 'active',
        orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
      };

      expect(await repo.getJob(job.id, { viewer: adminUser })).not.toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('GET /api/users/me/orgs', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });

  /**
   * Build a test app that mounts the REAL /api/users/me/orgs route via
   * mountUsersApi (the same entry point createCoreServer uses) and injects a
   * mocked req.user ahead of it. Pass `injectUser = null` to skip injection
   * and exercise requireAuth.
   */
  function buildApp(
    repo: Repository,
    injectUser: (Partial<Express.User> & { id: string }) | null,
  ): express.Application {
    const app = express();
    if (injectUser) {
      app.use((req: Request, _res: Response, next: NextFunction) => {
        (req as Request & { user: Express.User }).user = {
          email: 'u@x.com', name: 'u', avatarUrl: null, role: 'user', status: 'active',
          orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
          ...injectUser,
        } as Express.User;
        (req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => true;
        next();
      });
      // authActive=false: skip requireAuth (we pre-populate req.user above).
      mountUsersApi(app, repo, false);
    } else {
      // authActive=true: exercise the real requireAuth guard. isAuthenticated()
      // is missing so requireAuth should return 401.
      app.use((req: Request, _res: Response, next: NextFunction) => {
        (req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => false;
        next();
      });
      mountUsersApi(app, repo, true);
    }
    return app;
  }

  it('returns 401 when the request is unauthenticated (requireAuth gate)', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-orgs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const app = buildApp(repo, null);
      const res = await request(app).get('/api/users/me/orgs');
      expect(res.status).toBe(401);
      expect(res.body.error).toBe('Unauthorized');
    } finally {
      repo.close();
    }
  });

  it('returns the cached gitea orgs for the authenticated user', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-orgs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
      repo.replaceUserGiteaOrgs(alice.id, [
        { orgId: 'org-1', orgName: 'alpha' },
        { orgId: 'org-2', orgName: 'beta' },
      ]);

      const app = buildApp(repo, { id: alice.id });
      const res = await request(app).get('/api/users/me/orgs');
      expect(res.status).toBe(200);
      expect(res.body.orgs).toHaveLength(2);
      // listUserGiteaOrgs ORDERs by org_name ASC
      expect(res.body.orgs[0].orgName).toBe('alpha');
      expect(res.body.orgs[1].orgName).toBe('beta');
      expect(res.body.orgs[0].orgId).toBe('org-1');
    } finally {
      repo.close();
    }
  });

  it('returns empty array when user has no cached orgs', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-orgs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const bob = repo.createUser({ email: 'b@x.com', name: 'Bob', role: 'user', status: 'active' });
      const app = buildApp(repo, { id: bob.id });
      const res = await request(app).get('/api/users/me/orgs');
      expect(res.status).toBe(200);
      expect(res.body.orgs).toEqual([]);
    } finally {
      repo.close();
    }
  });
});

describe('PATCH /api/users/me/preferences', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });

  function buildApp(
    repo: Repository,
    injectUser: (Partial<Express.User> & { id: string }) | null,
  ): express.Application {
    const app = express();
    if (injectUser) {
      app.use((req: Request, _res: Response, next: NextFunction) => {
        (req as Request & { user: Express.User }).user = {
          email: 'u@x.com', name: 'u', avatarUrl: null, role: 'user', status: 'active',
          orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
          ...injectUser,
        } as Express.User;
        (req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => true;
        next();
      });
      mountUsersApi(app, repo, false);
    } else {
      app.use((req: Request, _res: Response, next: NextFunction) => {
        (req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => false;
        next();
      });
      mountUsersApi(app, repo, true);
    }
    return app;
  }

  it('returns 400 when defaultVisibility is invalid', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
      const app = buildApp(repo, { id: alice.id });
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'bogus' });
      expect(res.status).toBe(400);
      expect(res.body.error).toBe('invalid defaultVisibility');
    } finally {
      repo.close();
    }
  });

  it('returns 400 when defaultVisibilityOrgId is not one of the user orgs', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
      const app = buildApp(repo, { id: alice.id, orgIds: ['10'] });
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'org', defaultVisibilityOrgId: '99' });
      expect(res.status).toBe(400);
    } finally {
      repo.close();
    }
  });

  it('returns 400 when defaultVisibility=org is sent without defaultVisibilityOrgId', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
      const app = buildApp(repo, { id: alice.id, orgIds: ['10'] });
      for (const payload of [
        { defaultVisibility: 'org' },
        { defaultVisibility: 'org', defaultVisibilityOrgId: null },
        { defaultVisibility: 'org', defaultVisibilityOrgId: '' },
      ]) {
        const res = await request(app).patch('/api/users/me/preferences').send(payload);
        expect(res.status).toBe(400);
      }
      expect(repo.getUserById(alice.id)!.defaultVisibility).toBe('private');
    } finally {
      repo.close();
    }
  });

  it('writes preferences on valid input and persists them', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
      const app = buildApp(repo, { id: alice.id, orgIds: ['10'] });
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'org', defaultVisibilityOrgId: '10' });
      expect(res.status).toBe(200);
      expect(res.body.ok).toBe(true);
      const after = repo.getUserById(alice.id);
      expect(after!.defaultVisibility).toBe('org');
      expect(after!.defaultVisibilityOrgId).toBe('10');
    } finally {
      repo.close();
    }
  });

  it('returns 401 when unauthenticated', async () => {
    tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
    const repo = new Repository(join(tempDir, 'db.sqlite'));
    try {
      const app = buildApp(repo, null);
      const res = await request(app)
        .patch('/api/users/me/preferences')
        .send({ defaultVisibility: 'public' });
      expect(res.status).toBe(401);
    } finally {
      repo.close();
    }
  });
});
