import { describe, expect, it } from 'vitest';

import {
  buildHostingSnapshot,
  chooseAvailablePort,
  getStoredServicePorts,
  getTenantAppComposeProjectName,
  getTenantAppComposePath,
  getTenantPortRange,
  resolveServiceRemovalSubdomains,
  validateTenantCompose,
} from './app-hosting.js';

describe('app-hosting helpers', () => {
  it('builds hosting snapshot with explicit tenant port range metadata', async () => {
    const snapshot = await buildHostingSnapshot({
      id: 'john',
      port_range_start: 20800,
      port_range_size: 100,
    });

    expect(snapshot.tenantId).toBe('john');
    expect(snapshot.contractVersion).toBe(7);
    expect(snapshot.portRange).toEqual({
      start: 20800,
      end: 20899,
      size: 100,
      env: '20800-20899',
    });
    expect(snapshot.commands.appApply).toBe('app-apply');
    expect(snapshot.commands.appRemove).toBe('app-remove');
    expect(snapshot.paths.proxyAppSpec).toBe(
      '/workspace/apps/<app>/app.yml',
    );
    expect(snapshot.paths.ipcErrors).toBe('/workspace/ipc/errors/');
    expect(snapshot.paths.hostedAppStatusDir).toBe('/workspace/ipc/hosting-status/');
    expect(snapshot.notes).toContain(
      'Read /workspace/ipc/errors/ after a failed deploy before changing app.yml again',
    );
    expect(snapshot.notes).toContain(
      'Proxy apps deploy from /workspace/apps/<app>/app.yml using tenant-local rootless containerd, buildkitd, and nerdctl.',
    );
    expect(snapshot.notes).toContain(
      'Place hosted app source under /workspace/apps/<app>/, not /workspace/group/.',
    );
    expect(snapshot.notes).toContain(
      'Read /workspace/ipc/hosting-status/ for recent unit state and logs after deploy attempts',
    );
    expect(snapshot.notes).toContain(
      'If a user asks to build, create, scaffold, run, deploy, host, or expose an app, site, frontend, backend, or API, treat that as a hosted-app request by default.',
    );
    expect(snapshot.notes).toContain(
      'When a user asks to build or host an app, the task is not complete until app.yml exists and app-apply has been sent, unless they explicitly asked for local-only files.',
    );
    expect(snapshot.notes).toContain(
      'Do not stop at local source files or tell the user to run the app themselves with npm start, node, Docker Compose, or similar handoff steps unless they explicitly asked for local-only work.',
    );
    expect(snapshot.notes).toContain(
      'Host port allocation is platform-managed. Do not add ports: bindings to app.yml.',
    );
  });

  it('resolves per-app compose paths', () => {
    expect(getTenantAppComposePath('john', 'hello-app')).toBe(
      '/home/john/workspace/apps/hello-app/docker-compose.yml',
    );
  });

  it('builds tenant-scoped compose project names', () => {
    expect(getTenantAppComposeProjectName('John', 'JobRefs_App')).toBe(
      'nanoclaw-john-jobrefs-app',
    );
  });

  it('validates compose host ports against the required proxy port and tenant range', () => {
    const result = validateTenantCompose({
      tenantId: 'john',
      composeDir: '/home/john/workspace/apps/hello-app',
      composeContent: [
        'services:',
        '  app:',
        '    image: node:20-alpine',
        '    ports:',
        '      - "20801:3000"',
      ].join('\n'),
      requiredHostPort: 20801,
      portRange: getTenantPortRange({
        port_range_start: 20800,
        port_range_size: 100,
      }),
    });

    expect(result.valid).toBe(true);
    expect(result.hostPorts).toEqual([20801]);
  });

  it('rejects restricted compose features and mismatched proxy ports', () => {
    const result = validateTenantCompose({
      tenantId: 'john',
      composeDir: '/home/john/workspace/apps/hello-app',
      composeContent: [
        'services:',
        '  app:',
        '    image: nginx:alpine',
        '    network_mode: host',
        '    ports:',
        '      - "20802:80"',
      ].join('\n'),
      requiredHostPort: 20801,
      portRange: getTenantPortRange({
        port_range_start: 20800,
        port_range_size: 100,
      }),
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toContain('network_mode: host is not allowed.');
    expect(result.errors).toContain('network_mode is not allowed.');
    expect(result.errors).toContain(
      'Compose file must expose host port 20801 to match the requested proxy route.',
    );
  });

  it('rejects tenant-defined docker networks', () => {
    const result = validateTenantCompose({
      tenantId: 'john',
      composeDir: '/home/john/workspace/apps/hello-app',
      composeContent: [
        'services:',
        '  app:',
        '    image: nginx:alpine',
        '    ports:',
        '      - "20801:80"',
        '    networks:',
        '      - default',
        'networks:',
        '  default:',
        '    driver: bridge',
      ].join('\n'),
      requiredHostPort: 20801,
      portRange: getTenantPortRange({
        port_range_start: 20800,
        port_range_size: 100,
      }),
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Custom Docker networks are not allowed.');
    expect(result.errors).toContain('Service-level network configuration is not allowed.');
  });

  it('resolves compose-remove targets from either subdomain or legacy service name', () => {
    const storedServices = [
      {
        subdomain: 'hello',
        config: { name: 'hello-app', type: 'proxy', port: 20801 },
      },
      {
        subdomain: 'static-site',
        config: { name: 'static-site', type: 'static', root: 'static-site/public' },
      },
    ];

    expect(
      resolveServiceRemovalSubdomains(storedServices, [], 'hello-app'),
    ).toEqual(['hello']);
    expect(
      resolveServiceRemovalSubdomains(storedServices, ['static-site'], ''),
    ).toEqual(['static-site']);
  });

  it('collects stored host ports and chooses the next free port in range', () => {
    expect(
      getStoredServicePorts([
        { subdomain: 'hello', config: { port: 20805 } },
        { subdomain: 'world', config: { port: '20807' } },
        { subdomain: 'dupe', config: { port: 20805 } },
        { subdomain: 'bad', config: { port: 'nope' } },
      ]),
    ).toEqual([20805, 20807]);

    expect(
      chooseAvailablePort(
        { start: 20800, end: 20803, size: 4, env: '20800-20803' },
        [20800, 20802],
      ),
    ).toBe(20801);
    expect(
      chooseAvailablePort(
        { start: 20800, end: 20801, size: 2, env: '20800-20801' },
        [20800, 20801],
      ),
    ).toBeNull();
  });
});
