/**
 * Unit tests for src/middleware/opa.ts — Wasm mode and fail-closed edge cases.
 *
 * This file is kept separate from opa.test.ts because it needs different
 * `fs.existsSync` and `@open-policy-agent/opa-wasm` mock behaviour.
 *
 * Jest's module registry is isolated per test file, so the module-level
 * singletons (`wasmPolicy`, `scopesMap`) are fresh for each file.
 */

import path from 'path';
import { Request, Response, NextFunction } from 'express';
import { RequestHandler } from 'express';
import { ITokenPayload } from '../../../src/types/index';
import { AuthorizationError } from '../../../src/utils/errors';

// ─── Point POLICY_DIR at the real policies directory ─────────────────────────

const PROJECT_ROOT = path.resolve(__dirname, '../../..');
const POLICIES_DIR = path.join(PROJECT_ROOT, 'policies');

process.env['POLICY_DIR'] = POLICIES_DIR;

// ─── Wasm mock — a LoadedPolicy-like object ───────────────────────────────────

/** Tracks calls so individual tests can assert on evaluation results. */
const mockEvaluate = jest.fn();
const mockSetData = jest.fn();

const MOCK_LOADED_POLICY = {
  evaluate: mockEvaluate,
  setData: mockSetData,
};

// Mock @open-policy-agent/opa-wasm BEFORE the module is loaded
jest.mock('@open-policy-agent/opa-wasm', () => ({
  loadPolicy: jest.fn().mockResolvedValue(MOCK_LOADED_POLICY),
}));

// ─── Mock fs: existsSync returns true for .wasm AND scopes.json ──────────────

jest.mock('fs', () => {
  const actual = jest.requireActual<typeof import('fs')>('fs');
  return {
    ...actual,
    existsSync: jest.fn((_filePath: unknown) => {
      // Both .wasm and other paths exist
      return true;
    }),
    readFileSync: jest.fn((filePath: unknown, encoding?: unknown) => {
      if (typeof filePath === 'string' && filePath.endsWith('.wasm')) {
        // Return a Buffer-like object for the Wasm bundle
        return Buffer.from('fake-wasm-bytes');
      }
      // For scopes.json, delegate to the real fs
      return actual.readFileSync(filePath as string, encoding as BufferEncoding);
    }),
  };
});

// Import AFTER mocks
import { createOpaMiddleware, reloadOpaPolicy } from '../../../src/middleware/opa';

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeUser(scope: string): ITokenPayload {
  return {
    sub: 'agent-wasm-test',
    client_id: 'agent-wasm-test',
    scope,
    jti: 'jti-wasm-001',
    iat: 1000,
    exp: 9999999999,
  };
}

function makeReq(
  method: string,
  baseUrl: string,
  reqPath: string,
  user?: ITokenPayload,
): Partial<Request> {
  return { method, baseUrl, path: reqPath, user };
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('createOpaMiddleware (Wasm mode)', () => {
  let middleware: RequestHandler;
  let next: jest.MockedFunction<NextFunction>;

  beforeAll(async () => {
    middleware = await createOpaMiddleware();
  });

  beforeEach(() => {
    next = jest.fn();
    mockEvaluate.mockReset();
  });

  it('should load in Wasm mode and call setData with scopes.json', () => {
    // setData is called once during createOpaMiddleware() → loadWasmPolicy()
    expect(mockSetData).toHaveBeenCalledTimes(1);
  });

  it('should allow request when Wasm policy evaluate returns allow: true', () => {
    mockEvaluate.mockReturnValue([{ result: { allow: true } }]);

    const req = makeReq('GET', '/api/v1', '/agents', makeUser('agents:read')) as Request;
    middleware(req, {} as Response, next);

    expect(mockEvaluate).toHaveBeenCalledTimes(1);
    expect(next).toHaveBeenCalledWith(/* no args */);
  });

  it('should deny request when Wasm policy evaluate returns allow: false', () => {
    mockEvaluate.mockReturnValue([{ result: { allow: false } }]);

    const req = makeReq('GET', '/api/v1', '/agents', makeUser('agents:read')) as Request;
    middleware(req, {} as Response, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError));
  });

  it('should deny request when Wasm evaluate returns empty result set', () => {
    mockEvaluate.mockReturnValue([]);

    const req = makeReq('POST', '/api/v1', '/agents', makeUser('agents:write')) as Request;
    middleware(req, {} as Response, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError));
  });

  it('should deny request when Wasm evaluate returns non-array result', () => {
    mockEvaluate.mockReturnValue(null);

    const req = makeReq('GET', '/api/v1', '/audit', makeUser('audit:read')) as Request;
    middleware(req, {} as Response, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError));
  });

  it('should propagate unexpected errors thrown by Wasm evaluate to next', () => {
    const wasmError = new Error('Wasm evaluation failure');
    mockEvaluate.mockImplementation(() => { throw wasmError; });

    const req = makeReq('GET', '/api/v1', '/agents', makeUser('agents:read')) as Request;
    middleware(req, {} as Response, next);

    expect(next).toHaveBeenCalledWith(wasmError);
  });

  it('should call next(AuthorizationError) with "not authenticated" when req.user is absent in Wasm mode', () => {
    const req = makeReq('GET', '/api/v1', '/agents', undefined) as Request;
    middleware(req, {} as Response, next);

    const err = (next as jest.Mock).mock.calls[0][0] as AuthorizationError;
    expect(err).toBeInstanceOf(AuthorizationError);
    expect(err.message).toMatch(/not authenticated/i);
  });
});

describe('reloadOpaPolicy (Wasm mode)', () => {
  it('should reload in Wasm mode without error', async () => {
    await expect(reloadOpaPolicy()).resolves.toBeUndefined();
    // setData should have been called again during reload
    expect(mockSetData).toHaveBeenCalled();
  });
});
