import { afterEach, describe, it, expect, vi } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from '../db/repository.js';
import { requireAuth, requireAdmin, fetchGiteaOrgsForUser } from './auth.js';
import type { Request, Response, NextFunction } from 'express';

function mockReqRes(overrides: Partial<Request> = {}) {
  const req = {
    isAuthenticated: () => false,
    user: undefined,
    originalUrl: '/api/test',
    headers: { accept: 'application/json' },
    ...overrides,
  } as unknown as Request;
  const res = {
    status: vi.fn().mockReturnThis(),
    json: vi.fn().mockReturnThis(),
    redirect: vi.fn().mockReturnThis(),
  } as unknown as Response;
  const next = vi.fn() as NextFunction;
  return { req, res, next };
}

describe('requireAuth', () => {
  it('calls next() for authenticated active user', () => {
    const { req, res, next } = mockReqRes({
      isAuthenticated: () => true,
      user: { id: '1', role: 'user', status: 'active' },
    } as Partial<Request>);
    requireAuth(req, res, next);
    expect(next).toHaveBeenCalled();
  });

  it('returns 401 for unauthenticated API request', () => {
    const { req, res, next } = mockReqRes();
    requireAuth(req, res, next);
    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });

  it('redirects to /auth/login for unauthenticated non-API request', () => {
    const { req, res, next } = mockReqRes({
      originalUrl: '/ui',
      headers: { accept: 'text/html' },
    } as Partial<Request>);
    requireAuth(req, res, next);
    expect(res.redirect).toHaveBeenCalledWith('/auth/login');
  });
});

describe('requireAdmin', () => {
  it('calls next() for admin user', () => {
    const { req, res, next } = mockReqRes({
      isAuthenticated: () => true,
      user: { id: '1', role: 'admin', status: 'active' },
    } as Partial<Request>);
    requireAdmin(req, res, next);
    expect(next).toHaveBeenCalled();
  });

  it('returns 403 for non-admin user', () => {
    const { req, res, next } = mockReqRes({
      isAuthenticated: () => true,
      user: { id: '1', role: 'user', status: 'active' },
    } as Partial<Request>);
    requireAdmin(req, res, next);
    expect(res.status).toHaveBeenCalledWith(403);
  });

  it('returns 401 for unauthenticated request', () => {
    const { req, res, next } = mockReqRes();
    requireAdmin(req, res, next);
    expect(res.status).toHaveBeenCalledWith(401);
  });
});

describe('fetchGiteaOrgsForUser', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
    vi.restoreAllMocks();
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'auth-test-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('fetches and persists user orgs from Gitea API', async () => {
    const repo = makeRepo();
    try {
      const user = repo.createUser({
        email: 'a@gitea.local', name: 'alice', role: 'user', status: 'active',
      });
      vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
        ok: true,
        json: async () => [
          { id: 10, username: 'marketing' },
          { id: 20, username: 'platform' },
        ],
      } as Response);

      const orgIds = await fetchGiteaOrgsForUser(repo, user.id, 'http://gitea.local', 'token-xyz');

      expect(orgIds.sort()).toEqual(['10', '20']);
      expect(repo.listUserGiteaOrgs(user.id).map(o => o.orgName).sort())
        .toEqual(['marketing', 'platform']);
    } finally {
      repo.close();
    }
  });

  it('returns empty array on !res.ok', async () => {
    const repo = makeRepo();
    try {
      const user = repo.createUser({
        email: 'a@gitea.local', name: 'alice', role: 'user', status: 'active',
      });
      vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
        ok: false, status: 401,
      } as Response);

      const orgIds = await fetchGiteaOrgsForUser(repo, user.id, 'http://gitea.local', 'bad-token');

      expect(orgIds).toEqual([]);
      expect(repo.listUserGiteaOrgs(user.id)).toEqual([]);
    } finally {
      repo.close();
    }
  });

  it('returns empty array on fetch rejection (network error)', async () => {
    const repo = makeRepo();
    try {
      const user = repo.createUser({
        email: 'a@gitea.local', name: 'alice', role: 'user', status: 'active',
      });
      vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('ECONNREFUSED'));

      const orgIds = await fetchGiteaOrgsForUser(repo, user.id, 'http://gitea.local', 'token');

      expect(orgIds).toEqual([]);
      expect(repo.listUserGiteaOrgs(user.id)).toEqual([]);
    } finally {
      repo.close();
    }
  });

  it('clears stale cached orgs when fetch fails (prevents permission lag after org removal)', async () => {
    const repo = makeRepo();
    try {
      const user = repo.createUser({
        email: 'a@gitea.local', name: 'alice', role: 'user', status: 'active',
      });
      repo.replaceUserGiteaOrgs(user.id, [
        { orgId: '10', orgName: 'marketing' },
        { orgId: '20', orgName: 'platform' },
      ]);
      expect(repo.listUserGiteaOrgs(user.id)).toHaveLength(2);

      vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({ ok: false, status: 503 } as Response);
      await fetchGiteaOrgsForUser(repo, user.id, 'http://gitea.local', 'token');
      expect(repo.listUserGiteaOrgs(user.id)).toEqual([]);

      repo.replaceUserGiteaOrgs(user.id, [{ orgId: '10', orgName: 'marketing' }]);
      vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('ECONNREFUSED'));
      await fetchGiteaOrgsForUser(repo, user.id, 'http://gitea.local', 'token');
      expect(repo.listUserGiteaOrgs(user.id)).toEqual([]);
    } finally {
      repo.close();
    }
  });
});
