/**
 * Integration tests for plugin update delivery.
 *
 * Contract source: plugins/international-press-zone/docs/specs/
 * 2026-08-11-plugin-update-delivery-design.md — the shipped plugin pins it.
 */

jest.mock('../../../queue', () => ({
  translationQueue: {
    add: jest.fn().mockResolvedValue({ id: 'mock-job-id' }),
    process: jest.fn(),
    on: jest.fn(),
    close: jest.fn().mockResolvedValue(undefined),
  },
}));

import request from 'supertest';
import crypto from 'crypto';
import { Application } from 'express';
import { createServer } from '../../../server';
import { prismaMock } from '../../setup';
import { httpLogStream } from '../../../utils/logger';

const LICENSE_KEY = 'INTL-AAAA-BBBB-CCCC';
const SITE_URL = 'https://client.example.com';
const PACKAGE_SHA256 = crypto.createHash('sha256').update('package-bytes').digest('hex');
const CHECK_PATH = '/v1/international/updates/check';
const VERIFY_PATH = '/v1/international/updates/verify';

function licenseRow(overrides: Record<string, unknown> = {}) {
  return {
    id: '11111111-1111-1111-1111-111111111111',
    plugin: 'international',
    plan_tier: 'professional',
    status: 'active',
    sites_allowed: 5,
    languages_allowed: 5,
    expires_at: new Date(Date.now() + 30 * 24 * 3600 * 1000),
    activations: [{ site_url: SITE_URL }],
    ...overrides,
  } as any;
}

function releaseRow(overrides: Record<string, unknown> = {}) {
  return {
    id: '22222222-2222-2222-2222-222222222222',
    product: 'international-press-zone',
    plugin: 'international',
    version: '1.4.0',
    r2_object_key: 'international-press-zone/1.4.0/international-press-zone.zip',
    package_sha256: PACKAGE_SHA256,
    package_signature: 'ZGV0YWNoZWQtc2lnbmF0dXJl',
    signature_key_id: 'ipz-release-2026',
    requires_wp: '6.4',
    tested_wp: '6.7',
    requires_php: '8.1',
    homepage_url: 'https://press.zone/international',
    description: 'International Press Zone',
    changelog: '<h4>1.4.0</h4>',
    is_stable: true,
    published_at: new Date('2026-08-01T00:00:00Z'),
    ...overrides,
  } as any;
}

describe('Plugin update delivery', () => {
  let app: Application;

  beforeAll(() => {
    process.env.R2_ACCOUNT_ENDPOINT = 'https://account.r2.cloudflarestorage.com';
    process.env.R2_BUCKET = 'press-zone-releases';
    process.env.R2_ACCESS_KEY_ID = 'test-access-key-id';
    process.env.R2_SECRET_ACCESS_KEY = 'test-secret-access-key';
    process.env.R2_SIGNED_URL_TTL_SECONDS = '300';
    app = createServer();
  });

  describe('POST /v1/international/updates/check', () => {
    it('rejects an unauthenticated check without leaking release metadata', async () => {
      const response = await request(app).post(CHECK_PATH).send({ version: '1.3.0' });

      expect(response.status).toBe(400);
      expect(JSON.stringify(response.body)).not.toContain('1.4.0');
      expect(prismaMock.pluginRelease.findMany).not.toHaveBeenCalled();
    });

    it('rejects an unknown license exactly as the sibling licensing routes do', async () => {
      prismaMock.license.findUnique.mockResolvedValue(null);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.status).toBe(404);
      expect(response.body.error.code).toBe('INVALID_LICENSE');
      expect(prismaMock.pluginRelease.findMany).not.toHaveBeenCalled();
    });

    it('rejects an expired license', async () => {
      prismaMock.license.findUnique.mockResolvedValue(
        licenseRow({ expires_at: new Date(Date.now() - 1000) })
      );

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.status).toBe(403);
      expect(response.body.error.code).toBe('LICENSE_EXPIRED');
    });

    it('rejects a license that is not activated for the requesting site', async () => {
      prismaMock.license.findUnique.mockResolvedValue(
        licenseRow({ activations: [{ site_url: 'https://someone-else.example.com' }] })
      );

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.status).toBe(403);
      expect(response.body.error.code).toBe('SITE_NOT_ACTIVATED');
      expect(prismaMock.pluginRelease.findMany).not.toHaveBeenCalled();
    });

    it('reports no update when the installed version is already the latest', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow()]);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.4.0' });

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.update_available).toBe(false);
      expect(response.body.data.package).toBeUndefined();
    });

    it('compares versions numerically rather than lexically', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow({ version: '1.10.0' })]);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.9.0' });

      expect(response.body.data.update_available).toBe(true);
      expect(response.body.data.version).toBe('1.10.0');
    });

    it('offers the highest version even when a patch was published later', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([
        releaseRow({ version: '1.3.1', published_at: new Date('2026-08-09T00:00:00Z') }),
        releaseRow({ version: '1.4.0', published_at: new Date('2026-08-01T00:00:00Z') }),
      ]);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.body.data.update_available).toBe(true);
      expect(response.body.data.version).toBe('1.4.0');
    });

    it('keeps the license key out of the request log for the GET form', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow()]);
      const written: string[] = [];
      const writeSpy = jest
        .spyOn(httpLogStream, 'write')
        .mockImplementation((line: any) => {
          written.push(String(line));
          return true;
        });

      await request(app)
        .get(CHECK_PATH)
        .query({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });
      writeSpy.mockRestore();

      const log = written.join('\n');
      expect(log).toContain('[REDACTED]');
      expect(log).not.toContain(LICENSE_KEY);
    });

    it('returns every field the WordPress update transient needs', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow()]);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.status).toBe(200);
      expect(response.body.data).toMatchObject({
        update_available: true,
        version: '1.4.0',
        url: 'https://press.zone/international',
        tested: '6.7',
        requires: '6.4',
        requires_php: '8.1',
        description: 'International Press Zone',
        changelog: '<h4>1.4.0</h4>',
        sha256: PACKAGE_SHA256,
        signature: 'ZGV0YWNoZWQtc2lnbmF0dXJl',
        signature_key_id: 'ipz-release-2026',
      });
      expect(typeof response.body.data.package).toBe('string');
    });

    it('only offers a release the license plan tier is entitled to', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow({ plan_tier: 'starter' }));
      prismaMock.pluginRelease.findMany.mockResolvedValue([]);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.body.data.update_available).toBe(false);
      expect(prismaMock.pluginRelease.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            entitlements: { some: { plan_tier: 'starter' } },
          }),
        })
      );
    });

    it('mints a signed, expiring package URL that carries no license key', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow()]);

      const response = await request(app)
        .post(CHECK_PATH)
        .send({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      const packageUrl = new URL(response.body.data.package);
      expect(packageUrl.searchParams.get('X-Amz-Algorithm')).toBe('AWS4-HMAC-SHA256');
      expect(packageUrl.searchParams.get('X-Amz-Signature')).toMatch(/^[0-9a-f]{64}$/);
      expect(packageUrl.searchParams.get('X-Amz-Expires')).toBe('300');
      expect(response.body.data.package).not.toContain(LICENSE_KEY);
      expect(response.body.data.package).not.toContain('license');
    });

    it('accepts the transitional GET form for already-installed sites', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow()]);

      const response = await request(app)
        .get(CHECK_PATH)
        .query({ license_key: LICENSE_KEY, site_url: SITE_URL, version: '1.3.0' });

      expect(response.status).toBe(200);
      expect(response.body.data.update_available).toBe(true);
    });

    it('resolves the site from the plugin User-Agent when no site_url is sent', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findMany.mockResolvedValue([releaseRow()]);

      const response = await request(app)
        .post(CHECK_PATH)
        .set('User-Agent', `InternationalPressZone/1.3.0; ${SITE_URL}`)
        .send({ license_key: LICENSE_KEY, version: '1.3.0' });

      expect(response.status).toBe(200);
      expect(response.body.data.update_available).toBe(true);
    });

    it('fails closed when the site cannot be resolved at all', async () => {
      const response = await request(app)
        .post(CHECK_PATH)
        .set('User-Agent', 'curl/8.0')
        .send({ license_key: LICENSE_KEY, version: '1.3.0' });

      expect(response.status).toBe(403);
      expect(response.body.error.code).toBe('SITE_URL_REQUIRED');
      expect(prismaMock.license.findUnique).not.toHaveBeenCalled();
    });
  });

  describe('POST /v1/international/updates/verify', () => {
    it('confirms a package whose digest matches an entitled release', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findFirst.mockResolvedValue(releaseRow());

      const response = await request(app).post(VERIFY_PATH).send({
        license_key: LICENSE_KEY,
        site_url: SITE_URL,
        product: 'international-press-zone',
        version: '1.3.0',
        hash: PACKAGE_SHA256,
      });

      expect(response.status).toBe(200);
      expect(response.body.data).toMatchObject({
        valid: true,
        version: '1.4.0',
        sha256: PACKAGE_SHA256,
        signature: 'ZGV0YWNoZWQtc2lnbmF0dXJl',
        signature_key_id: 'ipz-release-2026',
      });
    });

    it('resolves the release by hash, never by the client-supplied version', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findFirst.mockResolvedValue(releaseRow());

      await request(app).post(VERIFY_PATH).send({
        license_key: LICENSE_KEY,
        site_url: SITE_URL,
        version: '1.3.0',
        hash: PACKAGE_SHA256,
      });

      const where = (prismaMock.pluginRelease.findFirst as jest.Mock).mock.calls[0][0].where;
      expect(where).toMatchObject({ package_sha256: PACKAGE_SHA256 });
      expect(JSON.stringify(where)).not.toContain('1.3.0');
    });

    it('returns a definite valid:false for an unknown digest', async () => {
      prismaMock.license.findUnique.mockResolvedValue(licenseRow());
      prismaMock.pluginRelease.findFirst.mockResolvedValue(null);

      const response = await request(app).post(VERIFY_PATH).send({
        license_key: LICENSE_KEY,
        site_url: SITE_URL,
        version: '1.3.0',
        hash: crypto.createHash('sha256').update('forged').digest('hex'),
      });

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.valid).toBe(false);
      expect(response.body.data).toHaveProperty('valid');
      expect(response.body.data.signature).toBeUndefined();
    });

    it('rejects a malformed digest rather than answering ambiguously', async () => {
      const response = await request(app).post(VERIFY_PATH).send({
        license_key: LICENSE_KEY,
        site_url: SITE_URL,
        version: '1.3.0',
        hash: 'not-a-sha256',
      });

      expect(response.status).toBe(400);
      expect(response.body.data).toBeUndefined();
    });

    it('rejects verification for a site the license is not activated on', async () => {
      prismaMock.license.findUnique.mockResolvedValue(
        licenseRow({ activations: [{ site_url: 'https://someone-else.example.com' }] })
      );

      const response = await request(app).post(VERIFY_PATH).send({
        license_key: LICENSE_KEY,
        site_url: SITE_URL,
        version: '1.3.0',
        hash: PACKAGE_SHA256,
      });

      expect(response.status).toBe(403);
      expect(response.body.error.code).toBe('SITE_NOT_ACTIVATED');
      expect(response.body.data).toBeUndefined();
    });
  });
});
