import { assertEquals, assertThrows } from '@std/assert';

import { AppStoreManager } from '../ts/classes/appstore.ts';
import { OneboxDockerManager } from '../ts/classes/docker.ts';
import type * as servezoneInterfaces from '@serve.zone/interfaces';
import type { IService } from '../ts/types.ts';

type IAppStoreVersionConfig = servezoneInterfaces.appstore.IAppStoreVersionConfig;

const createAppStore = () => new AppStoreManager({} as any);

const baseConfig: IAppStoreVersionConfig = {
  image: 'example/app:1.0.0',
  port: 3000,
  envVars: [
    {
      key: 'APP_PORT',
      value: '3000',
      description: 'Application port',
      required: true,
    },
  ],
};

const baseService: IService = {
  id: 1,
  name: 'test-service',
  image: 'example/app:1.0.0',
  envVars: {},
  port: 3000,
  status: 'stopped',
  createdAt: Date.now(),
  updatedAt: Date.now(),
};

Deno.test('appstore normalizes and validates app template runtime fields', () => {
  const appStore = createAppStore();

  const normalizedVolumes = appStore.normalizeVolumes([
    '/data/app',
    { mountPath: '/config', readOnly: true },
  ]);

  assertEquals(normalizedVolumes, [
    { mountPath: '/data/app' },
    { mountPath: '/config', readOnly: true },
  ]);

  appStore.validateAppVersionConfig({
    ...baseConfig,
    volumes: normalizedVolumes,
    publishedPorts: [
      { targetPort: 3000, publishedPort: 3000, protocol: 'tcp' },
      { targetPort: 20000, targetPortEnd: 20002, publishedPort: 20000, publishedPortEnd: 20002, protocol: 'udp' },
    ],
  });
});

Deno.test('appstore rejects invalid template ports and volumes', () => {
  const appStore = createAppStore();

  assertThrows(
    () => appStore.validateAppVersionConfig({ ...baseConfig, port: 70000 }),
    Error,
    'Invalid app config port',
  );

  assertThrows(
    () => appStore.normalizeVolumes([{ mountPath: 'relative/path' }]),
    Error,
    'mountPath must be an absolute path',
  );

  assertThrows(
    () => appStore.validateAppVersionConfig({
      ...baseConfig,
      publishedPorts: [
        { targetPort: 3000, targetPortEnd: 3002, publishedPort: 3000, publishedPortEnd: 3001, protocol: 'tcp' },
      ],
    }),
    Error,
    'ranges must have the same size',
  );
});

Deno.test('appstore resolves repo manifests and docker digest-tracked latest images', async () => {
  const appStoreBaseUrl = 'https://appstore.example.test';
  const manifestUrl = 'https://code.example.test/cloudly/servezone.appstore.json';
  const digest = 'sha256:1234567890abcdef';

  const fakeFetch: typeof fetch = async (input, init) => {
    const url = input instanceof Request ? input.url : input.toString();
    const method = init?.method || 'GET';

    if (url === `${appStoreBaseUrl}/appstore.resolved.json`) {
      return new Response('not found', { status: 404 });
    }

    if (url === `${appStoreBaseUrl}/appstore.json`) {
      return Response.json({
        schemaVersion: 1,
        updatedAt: '2026-05-24T00:00:00Z',
        apps: [
          {
            id: 'cloudly',
            name: 'Cloudly',
            description: 'Central metadata can stay curated.',
            category: 'Dev Tools',
            latestVersion: '1.0.0',
            source: {
              type: 'repoManifest',
              url: manifestUrl,
              ref: 'main',
            },
          },
        ],
      });
    }

    if (url === manifestUrl) {
      return Response.json({
        schemaVersion: 1,
        app: {
          id: 'cloudly',
          name: 'Cloudly',
          description: 'Manifest-owned app metadata.',
          category: 'Dev Tools',
          maintainer: 'serve.zone',
        },
        latestVersion: 'latest',
        source: {
          type: 'dockerImage',
          image: 'registry.example.test/serve.zone/cloudly:latest',
          tracking: 'digest',
        },
        runtime: {
          image: 'registry.example.test/serve.zone/cloudly:latest',
          port: 80,
        },
      });
    }

    if (
      url === 'https://registry.example.test/v2/serve.zone/cloudly/manifests/latest' &&
      method === 'HEAD'
    ) {
      return new Response(null, {
        status: 200,
        headers: { 'docker-content-digest': digest },
      });
    }

    return new Response(`unexpected ${method} ${url}`, { status: 500 });
  };

  const appStore = new AppStoreManager({} as any, {
    baseUrl: appStoreBaseUrl,
    fetch: fakeFetch,
  });

  const appStoreIndex = await appStore.getAppStore();
  assertEquals(appStoreIndex.apps[0].latestVersion, `latest@${digest}`);
  assertEquals(appStoreIndex.apps[0].resolvedSource?.manifestHash?.length, 64);
  assertEquals(appStoreIndex.apps[0].upgradeStrategy, 'dockerDigest');

  const appMeta = await appStore.getAppMeta('cloudly');
  assertEquals(appMeta.latestVersion, `latest@${digest}`);
  assertEquals(appMeta.versions, [`latest@${digest}`]);

  const config = await appStore.getAppVersionConfig('cloudly', appMeta.latestVersion);
  assertEquals(config.image, 'registry.example.test/serve.zone/cloudly:latest');
  assertEquals(config.appStoreVersion, `latest@${digest}`);
  assertEquals(config.resolvedImageDigest, digest);
});

Deno.test('docker service spec validation rejects unsafe volume and port declarations', () => {
  const dockerManager = new OneboxDockerManager();

  dockerManager.validateServiceSpec({
    ...baseService,
    volumes: [{ mountPath: '/data/app' }],
    publishedPorts: [{ targetPort: 3000, publishedPort: 3000, protocol: 'tcp' }],
  });

  assertThrows(
    () => dockerManager.validateServiceSpec({
      ...baseService,
      volumes: [{ mountPath: 'relative/path' }],
    }),
    Error,
    'must be an absolute path',
  );

  assertThrows(
    () => dockerManager.validateServiceSpec({
      ...baseService,
      publishedPorts: [
        { targetPort: 3001, publishedPort: 3000, hostIp: '127.0.0.1', protocol: 'tcp' },
        { targetPort: 3000, publishedPort: 3000, protocol: 'tcp' },
      ],
    }),
    Error,
    'Duplicate published port',
  );
});
