/**
 * Locks the frontend scheduled-action registry: the task-to-definition
 * resolution (including the update-fleet alias), and the parity between the
 * registry's backend actions and the wire action union.
 */
import { describe, it, expect } from 'vitest';
import {
  SCHEDULED_ACTIONS,
  SCHEDULED_ACTION_CATEGORIES,
  DEFAULT_SCHEDULED_ACTION_ID,
  getActionById,
  resolveTaskAction,
  scheduleTargetDescriptor,
  stripComposeExt,
  RISK_LABEL,
  RISK_TONE,
  RISK_BADGE_CLASSES,
  RISK_DOT_CLASSES,
  type BackendAction,
  type ScheduledActionCategory,
  type ScheduledActionRiskLevel,
} from '../scheduledActions';
import type { ScheduledTask } from '@/types/scheduling';

type TargetTask = Pick<ScheduledTask, 'action' | 'target_type' | 'target_id' | 'name'>;

const BACKEND_ACTIONS: BackendAction[] = [
  'restart', 'snapshot', 'prune', 'update', 'scan',
  'auto_backup', 'auto_stop', 'auto_down', 'auto_start',
];

const CATEGORY_KEYS: ScheduledActionCategory[] = SCHEDULED_ACTION_CATEGORIES.map(c => c.key);

describe('scheduledActions registry', () => {
  it('every backendAction is a known wire action', () => {
    for (const def of SCHEDULED_ACTIONS) {
      expect(BACKEND_ACTIONS).toContain(def.backendAction);
    }
  });

  it('covers every wire action with at least one entry', () => {
    const covered = new Set(SCHEDULED_ACTIONS.map(d => d.backendAction));
    expect([...covered].sort()).toEqual([...BACKEND_ACTIONS].sort());
  });

  it('every entry uses a defined category lane', () => {
    for (const def of SCHEDULED_ACTIONS) {
      expect(CATEGORY_KEYS).toContain(def.category);
    }
  });

  describe('timeline lanes', () => {
    it('pins each lane key, order, and label so a registry rewrite cannot silently rename or reorder one', () => {
      expect(SCHEDULED_ACTION_CATEGORIES.map(lane => [lane.key, lane.label])).toEqual([
        ['lifecycle', 'Lifecycle'],
        ['updates', 'Updates'],
        ['security', 'Security'],
        ['maintenance', 'Upkeep'],
        ['backups', 'Backups'],
      ]);
    });

    it('gives every lane a non-empty color and background so its Timeline dot always renders', () => {
      for (const lane of SCHEDULED_ACTION_CATEGORIES) {
        expect(lane.color.trim()).not.toBe('');
        expect(lane.bg.trim()).not.toBe('');
      }
    });
  });

  it('marks scan and prune as local-node actions', () => {
    expect(getActionById('scan')).toMatchObject({ requiresNode: true, nodeScope: 'local' });
    expect(getActionById('prune')).toMatchObject({ requiresNode: true, nodeScope: 'local' });
  });

  it('getActionById resolves a known id and returns undefined otherwise', () => {
    expect(getActionById('restart')?.label).toBe('Restart Stack');
    expect(getActionById('nope')).toBeUndefined();
  });

  describe('resolveTaskAction', () => {
    it('maps update + fleet + stack-label selector to update-by-label', () => {
      const def = resolveTaskAction({
        action: 'update',
        target_type: 'fleet',
        selector_type: 'stack-label',
      });
      expect(def?.id).toBe('update-by-label');
      expect(def?.backendAction).toBe('update');
    });

    it('maps update + fleet without selector to update-fleet', () => {
      const def = resolveTaskAction({ action: 'update', target_type: 'fleet', selector_type: null });
      expect(def?.id).toBe('update-fleet');
    });

    it('maps update + fleet to the update-fleet UI entry', () => {
      const def = resolveTaskAction({ action: 'update', target_type: 'fleet' });
      expect(def?.id).toBe('update-fleet');
      expect(def?.backendAction).toBe('update');
    });

    it('maps update + stack to the direct update entry', () => {
      const def = resolveTaskAction({ action: 'update', target_type: 'stack' });
      expect(def?.id).toBe('update');
    });

    it('maps container target types to container UI entries', () => {
      expect(resolveTaskAction({ action: 'restart', target_type: 'container' })?.id).toBe('container-restart');
      expect(resolveTaskAction({ action: 'auto_stop', target_type: 'container' })?.id).toBe('container-stop');
      expect(resolveTaskAction({ action: 'auto_start', target_type: 'container' })?.id).toBe('container-start');
    });

    it('maps a non-aliased action to its direct entry', () => {
      expect(resolveTaskAction({ action: 'restart', target_type: 'stack' })?.id).toBe('restart');
      expect(resolveTaskAction({ action: 'snapshot', target_type: 'fleet' })?.id).toBe('snapshot');
    });

    it('returns undefined for an unknown action', () => {
      expect(resolveTaskAction({ action: 'bogus' as BackendAction, target_type: 'system' })).toBeUndefined();
    });
  });

  it('exports DEFAULT_SCHEDULED_ACTION_ID as restart', () => {
    expect(DEFAULT_SCHEDULED_ACTION_ID).toBe('restart');
  });

  it('orders actions by category group', () => {
    const ids = SCHEDULED_ACTIONS.map(a => a.id);
    // Verify the exact order: lifecycle first, then updates, security, maintenance, backups.
    expect(ids).toEqual([
      'auto_backup', 'auto_start', 'restart', 'auto_stop', 'auto_down',
      'container-restart', 'container-stop', 'container-start',
      'update', 'update-fleet', 'update-by-label',
      'scan',
      'prune',
      'snapshot',
    ]);
  });

  it('preserves the update-fleet alias after resorting', () => {
    const fleetUpdate = SCHEDULED_ACTIONS.find(a => a.id === 'update-fleet');
    expect(fleetUpdate).toBeDefined();
    expect(fleetUpdate!.backendAction).toBe('update');
    expect(fleetUpdate!.targetType).toBe('fleet');
  });

  it('preserves the update-by-label alias', () => {
    const byLabel = SCHEDULED_ACTIONS.find(a => a.id === 'update-by-label');
    expect(byLabel).toBeDefined();
    expect(byLabel!.backendAction).toBe('update');
    expect(byLabel!.targetType).toBe('fleet');
    expect(byLabel!.requiresNode).toBe(false);
  });

  describe('helperText', () => {
    const expected: Record<string, string> = {
      'auto_backup': 'Backs up compose and env files only. This does not back up application volumes.',
      'auto_start': 'Creates containers if they do not exist, or starts existing stopped containers.',
      'restart': 'Restarts containers in place. Running services are stopped and started again on the same configuration.',
      'auto_stop': 'Stops containers but keeps them in place for a faster start later.',
      'auto_down': 'Runs compose down. Containers are removed, but compose files remain on disk.',
      'container-restart': 'Restarts a single container by name on the selected node. Targets the container directly, not through compose.',
      'container-stop': 'Stops a single container by name. The container remains on disk for a faster start later.',
      'container-start': 'Starts a stopped container by name on the selected node.',
      'update': "Checks this stack's images and recreates the stack only when newer images are available.",
      'update-fleet': 'Checks every stack on the selected node and updates stacks with newer images.',
      'update-by-label': 'Resolves stacks that currently carry a Stack Label at each run, across the entire fleet or one node, and updates those with newer images.',
      'scan': 'Runs Trivy against images on the selected local node and records the findings.',
      'prune': 'Removes unused Docker resources on the selected node. Be careful when pruning volumes.',
      'snapshot': 'Creates a versioned snapshot of compose and env files across the fleet.',
    };

    for (const [id, text] of Object.entries(expected)) {
      it(`${id} helper text matches the specified wording`, () => {
        expect(getActionById(id)?.helperText).toBe(text);
      });
    }
  });

  describe('riskLevel', () => {
    const expected: Record<string, ScheduledActionRiskLevel> = {
      'auto_backup': 'safe',
      'auto_start': 'runtime-change',
      'restart': 'interruptive',
      'auto_stop': 'interruptive',
      'auto_down': 'removes-containers',
      'container-restart': 'interruptive',
      'container-stop': 'interruptive',
      'container-start': 'runtime-change',
      'update': 'runtime-change',
      'update-fleet': 'runtime-change',
      'update-by-label': 'runtime-change',
      'scan': 'read-only',
      'prune': 'destructive',
      'snapshot': 'safe',
    };

    for (const [id, level] of Object.entries(expected)) {
      it(`${id} risk level is ${level}`, () => {
        expect(getActionById(id)?.riskLevel).toBe(level);
      });
    }

    it('every action has a RISK_LABEL entry', () => {
      for (const def of SCHEDULED_ACTIONS) {
        expect(RISK_LABEL[def.riskLevel]).toBeTruthy();
      }
    });
  });

  describe('risk metadata maps', () => {
    it('RISK_TONE maps every action risk level', () => {
      for (const def of SCHEDULED_ACTIONS) {
        expect(RISK_TONE[def.riskLevel]).toBeTruthy();
      }
    });

    it('RISK_BADGE_CLASSES maps every action risk level', () => {
      for (const def of SCHEDULED_ACTIONS) {
        expect(RISK_BADGE_CLASSES[def.riskLevel]).toBeTruthy();
      }
    });

    it('RISK_DOT_CLASSES maps every action risk level', () => {
      for (const def of SCHEDULED_ACTIONS) {
        expect(RISK_DOT_CLASSES[def.riskLevel]).toBeTruthy();
      }
    });
  });

  it('resolveTaskAction for update+fleet returns correct metadata', () => {
    const def = resolveTaskAction({ action: 'update', target_type: 'fleet' });
    expect(def?.id).toBe('update-fleet');
    expect(def?.riskLevel).toBe('runtime-change');
    expect(def?.helperText).toBe('Checks every stack on the selected node and updates stacks with newer images.');
  });

  describe('stripComposeExt', () => {
    it('drops a trailing .yml or .yaml and leaves other names alone', () => {
      expect(stripComposeExt('web')).toBe('web');
      expect(stripComposeExt('web.yml')).toBe('web');
      expect(stripComposeExt('web.yaml')).toBe('web');
      expect(stripComposeExt('')).toBe('');
      expect(stripComposeExt('my.app')).toBe('my.app');
    });
  });

  describe('scheduleTargetDescriptor', () => {
    it('shows the stack name (without compose extension) for stack actions', () => {
      const task: TargetTask = { action: 'restart', target_type: 'stack', target_id: 'web.yml', name: 'Nightly restart' };
      expect(scheduleTargetDescriptor(task, 'hub')).toBe('web');
    });

    it('falls back to the task name when a stack target_id is missing', () => {
      const task: TargetTask = { action: 'restart', target_type: 'stack', target_id: null, name: 'api.yaml' };
      expect(scheduleTargetDescriptor(task)).toBe('api');
    });

    it('shows the node for a fleet auto-update, or a generic label without one', () => {
      const task: TargetTask = { action: 'update', target_type: 'fleet', target_id: null, name: 'Fleet update' };
      expect(scheduleTargetDescriptor(task, 'edge-1')).toBe('All stacks · edge-1');
      expect(scheduleTargetDescriptor(task)).toBe('All stacks');
    });

    it('shows Label: name · Entire fleet or node for stack-label selectors', () => {
      const fleet = {
        action: 'update' as const,
        target_type: 'fleet' as const,
        target_id: null,
        name: 'Label update',
        selector_type: 'stack-label',
        selector_value: 'Production',
        node_id: null as number | null,
      };
      expect(scheduleTargetDescriptor(fleet)).toBe('Label: Production · Entire fleet');
      const scoped = { ...fleet, node_id: 7 };
      expect(scheduleTargetDescriptor(scoped, 'Node A')).toBe('Label: Production · Node A');
      expect(scheduleTargetDescriptor(scoped)).toBe('Label: Production · node 7');
    });

    it('shows Entire fleet for a fleet snapshot regardless of node', () => {
      const task: TargetTask = { action: 'snapshot', target_type: 'fleet', target_id: null, name: 'Nightly Snapshot' };
      expect(scheduleTargetDescriptor(task, 'edge-1')).toBe('Entire fleet');
      expect(scheduleTargetDescriptor(task)).toBe('Entire fleet');
    });

    it('shows the container name for container actions', () => {
      const task: TargetTask = {
        action: 'restart',
        target_type: 'container',
        target_id: 'watchtower',
        name: 'Daily watchtower restart',
      };
      expect(scheduleTargetDescriptor(task, 'hub')).toBe('watchtower');
    });

    it('shows the node for system actions (prune / scan), with a fallback', () => {
      const scan: TargetTask = { action: 'scan', target_type: 'system', target_id: null, name: 'Vul Scan' };
      const prune: TargetTask = { action: 'prune', target_type: 'system', target_id: null, name: 'Nightly Prune' };
      expect(scheduleTargetDescriptor(scan, 'hub')).toBe('hub');
      expect(scheduleTargetDescriptor(prune, 'edge-1')).toBe('edge-1');
      expect(scheduleTargetDescriptor(scan)).toBe('Selected node');
    });
  });
});
