/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ansi } from "@nacho-iot/js-tools";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Docker } from "../docker/docker.js";
import { edit } from "../docker/edit.js";
import { Image } from "../docker/image.js";
import { Volume } from "../docker/volume.js";
import { afterRun, beforeRun } from "../mocha.js";
import { TestRunner } from "../runner.js";
import { TestDescriptor } from "../test-descriptor.js";
import { AccessoryServer } from "./accessory-server.js";
import { Constants, ContainerPaths } from "./config.js";
import { ContainerCommandPipe } from "./container-command-pipe.js";
import { PicsSource } from "./pics/source.js";
import { PythonTest } from "./python-test.js";
import { YamlTest } from "./yaml-test.js";
const Values = {
  isInitialized: false,
  subject: void 0,
  test: void 0,
  mainContainer: void 0,
  mdnsContainer: void 0,
  defaultPics: void 0,
  defaultPicsFilename: void 0,
  tests: void 0,
  initializedSubjects: /* @__PURE__ */ new WeakSet(),
  activeSubject: void 0,
  singleUseSubject: false,
  closers: Array(),
  subjects: /* @__PURE__ */ new Map(),
  subjectsByApp: /* @__PURE__ */ new Map(),
  snapshots: /* @__PURE__ */ new Map(),
  containerLifecycleInstalled: false,
  testMap: /* @__PURE__ */ new Map(),
  pullBeforeTesting: true,
  commandPipe: void 0,
  localControllerPort: void 0,
  restartFlagHostDir: void 0
};
const State = {
  get container() {
    const container = Values.mainContainer;
    if (container === void 0) {
      throw new Error("Docker container is not initialized");
    }
    return container;
  },
  set subject(subject) {
    Values.subject = subject;
  },
  get subject() {
    const subject = Values.subject;
    if (subject === void 0) {
      throw new Error("no default subject configured");
    }
    return subject;
  },
  registerSubjectForApp(app, factory) {
    if (Values.subjectsByApp.has(app)) {
      throw new Error(`Subject factory already registered for app "${app}"`);
    }
    Values.subjectsByApp.set(app, factory);
  },
  subjectForApp(app) {
    return Values.subjectsByApp.get(app);
  },
  get pullBeforeTesting() {
    return Values.pullBeforeTesting;
  },
  set pullBeforeTesting(value) {
    Values.pullBeforeTesting = value;
  },
  get defaultPics() {
    if (Values.defaultPics === void 0) {
      throw new Error("PICS not initialized");
    }
    return Values.defaultPics;
  },
  get defaultPicsFilename() {
    if (Values.defaultPicsFilename === void 0) {
      throw new Error("PICS not initialized");
    }
    return Values.defaultPicsFilename;
  },
  get tests() {
    if (Values.tests === void 0) {
      throw new Error("CHIP test descriptor not loaded");
    }
    return Values.tests;
  },
  get test() {
    if (Values.test === void 0) {
      throw new Error("No active test");
    }
    return Values.test;
  },
  get localControllerPort() {
    return Values.localControllerPort;
  },
  get restartFlagHostDir() {
    return Values.restartFlagHostDir;
  },
  get isInitialized() {
    return Values.isInitialized;
  },
  /**
   * Setup.
   */
  async initialize() {
    if (Values.isInitialized) {
      return;
    }
    const { progress } = TestRunner.current;
    progress.update("Initializing containers");
    try {
      await initialize();
      const image = await State.container.image;
      const info = await image.inspect();
      const chipCommit = formatSha(info.Config.Labels["org.opencontainers.image.revision"] ?? "(unknown)");
      const imageVersion = info.Config.Labels["org.opencontainers.image.version"] ?? "(unknown)";
      const arch = info.Architecture;
      progress.success(
        `Initialized CHIP ${ansi.bold(chipCommit)} image ${ansi.bold(imageVersion)} for ${ansi.bold(arch)}`
      );
    } catch (e) {
      progress.failure("Initializing containers");
      throw e;
    }
  },
  /**
   * Teardown.
   */
  async close() {
    await State.deactivateSubject();
    let closer;
    while (closer = Values.closers.pop()) {
      try {
        await closer();
      } catch (e) {
        console.error("Teardown error:", e);
      }
    }
    Values.isInitialized = false;
  },
  /**
   * Add cleanup logic.
   */
  onClose(fn) {
    Values.closers.push(fn);
  },
  /**
   * Hook mocha to initialize CHIP testing.
   */
  install() {
    if (Values.containerLifecycleInstalled) {
      return;
    }
    Values.containerLifecycleInstalled = true;
    beforeRun(State.initialize);
    afterRun(State.close);
  },
  /**
   * Run a CHIP test.
   */
  async run(test, args, beforeTest) {
    const { reporter } = TestRunner.current;
    const subject = Values.activeSubject;
    try {
      Values.test = test;
      await beforeTest(subject, test);
      await test.invoke(subject, reporter.beginStep.bind(reporter), args);
    } finally {
      Values.test = void 0;
    }
  },
  /**
   * Pass a backchannel command to the active subject.
   */
  backchannel(command) {
    if (Values.activeSubject === void 0) {
      throw new Error(`Backchannel ${command.name} without active test subject`);
    }
    return Values.activeSubject.backchannel(command);
  },
  /**
   * Open a back-channel command pipe.
   */
  async openPipe(name) {
    if (Values.commandPipe === void 0) {
      Values.commandPipe = new ContainerCommandPipe(State.container, this);
      await Values.commandPipe.initialize();
      State.onClose(async () => {
        await Values.commandPipe?.close();
        Values.commandPipe = void 0;
      });
    }
    await Values.commandPipe.installForApp(name);
  },
  /**
   * Obtain a {@link Test}.
   */
  testFor(identifier) {
    if (typeof identifier === "string") {
      const maybeDescriptor = this.tests.stat(identifier)?.descriptor;
      if (maybeDescriptor === void 0) {
        throw new Error(`No such test ${identifier}`);
      }
      identifier = maybeDescriptor;
    }
    let test = Values.testMap.get(identifier);
    if (!test) {
      test = createTest(identifier);
      Values.testMap.set(identifier, test);
    }
    return test;
  },
  /**
   * Prepare the test environment for a subject.
   *
   * On first activation, commissions the subject.  Thereafter the subject is either already active or reactivated
   * here.
   */
  async activateSubject(factory, startCommissioned, test, beforeStart, appArgs) {
    let subject;
    if (startCommissioned) {
      subject = loadSubject(factory, test.descriptor.kind, appArgs);
    } else {
      subject = factory(test.descriptor.kind ?? "unknown", { appArgs });
    }
    if (Values.activeSubject === subject) {
      return;
    }
    const { progress } = TestRunner.current;
    await progress.subtask("activating subject", async () => {
      await State.container.exec(["bash", "-c", 'export GLOBIGNORE="/tmp/*_fifo_*"; rm -rf /tmp/*']);
      if (!startCommissioned) {
        await subject.initialize();
        State.onClose(subject.close.bind(subject));
        await beforeStart?.(subject, test);
        await subject.start();
      } else if (!Values.initializedSubjects.has(subject)) {
        await subject.initialize();
        State.onClose(subject.close.bind(subject));
        await beforeStart?.(subject, test);
        await subject.start();
        await test.initializeSubject(subject);
        const dir = storageDirFor(subject);
        Values.snapshots.set(subject, await subject.snapshot());
        await State.container.exec(["bash", "-c", `mkdir -p ${dir} && cp -a /tmp/* ${dir}`]);
        Values.initializedSubjects.add(subject);
      } else {
        const snapshot = Values.snapshots.get(subject);
        if (snapshot === void 0) {
          throw new Error(`No snapshot captured for ${subject.id}`);
        }
        await subject.restore(snapshot);
        await State.container.exec(["bash", "-c", `cp -a ${storageDirFor(subject)}/* /tmp`]);
        await beforeStart?.(subject, test);
        await subject.start();
      }
    });
    Values.activeSubject = subject;
    Values.singleUseSubject = !startCommissioned;
  },
  /**
   * Stop the current test subject, if any.
   *
   * This stops the subject but leaves it initialized (commissioned).  This allows us to quickly swap subjects depending
   * on the current test.
   *
   * Final teardown of subjects occurs once all tests complete.
   */
  async deactivateSubject() {
    if (Values.activeSubject === void 0) {
      return;
    }
    try {
      if (Values.singleUseSubject) {
        await Values.activeSubject.close();
      } else {
        await Values.activeSubject.stop();
      }
    } catch (e) {
      console.warn("Error deactivating test subject", e);
    } finally {
      Values.activeSubject = void 0;
    }
  },
  /**
   * Clear the MDNS cache.
   */
  async clearMdns() {
    if (!Values.mdnsContainer) {
      throw new Error("Cannot reset MDNS because MDNS container is not initialized");
    }
    await this.deactivateSubject();
    await Values.mdnsContainer.exec("/bin/mdns-clear");
  }
};
async function initialize() {
  await configureContainer();
  await configureScripts();
  await configurePics();
  await configureTests();
  await configureNetwork();
  if (Constants.useLocalController) {
    await configureLocalController();
  }
  Values.isInitialized = true;
}
async function configureContainer() {
  const docker = new Docker();
  let platform = Constants.platform;
  if (Values.pullBeforeTesting) {
    await docker.pull(Constants.imageName, platform);
  } else if (Constants.selectedPlatform === void 0) {
    const arch = (await Image(docker, Constants.imageName).inspect()).Architecture;
    platform = `linux/${arch}`;
  }
  const mdnsVolume = Volume(docker, Constants.mdnsVolumeName);
  await mdnsVolume.open();
  const restartFlagHostDir = await mkdtemp(join(tmpdir(), "matter-restart-"));
  Values.restartFlagHostDir = restartFlagHostDir;
  const composition = docker.compose("matter.js", {
    image: Constants.imageName,
    platform,
    binds: { [mdnsVolume.name]: "/run/dbus" },
    autoRemove: true,
    // Meh.  Don't have non-host network working yet
    network: "host"
    //Network(docker, Constants.networkName),
  });
  await composition.add({
    name: "dbus",
    command: ["/usr/bin/dbus-daemon", "--nopidfile", "--system", "--nofork"]
  });
  Values.mdnsContainer = await composition.add({
    name: "mdns",
    command: ["/bin/mdns-run"]
  });
  Values.mainContainer = await composition.add({
    name: "chip",
    recreate: true,
    binds: {
      [mdnsVolume.name]: "/run/dbus",
      [restartFlagHostDir]: Constants.RestartFlagDir
    }
  });
  State.onClose(async () => {
    try {
      await composition.close();
    } catch (e) {
      console.error("Error terminating containers:", e);
    }
    Values.mainContainer = void 0;
    try {
      await rm(restartFlagHostDir, { recursive: true, force: true });
    } catch (e) {
      console.warn("Error cleaning up restart flag directory:", e);
    }
    Values.restartFlagHostDir = void 0;
  });
}
async function configureScripts() {
  await State.container.edit(
    edit.sed("s/sleep(0.001)/sleep(.02)/"),
    // This is the one we actually use
    "/usr/local/lib/python3.12/dist-packages/matter/testing/matter_testing.py",
    // Patching here too just for completeness
    "/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py"
  );
}
async function configurePics() {
  Values.defaultPics = await PicsSource.load(Constants.defaultPics);
  Values.defaultPicsFilename = await PicsSource.install(Values.defaultPics);
}
async function configureTests() {
  const { container } = State;
  const descriptor = JSON.parse(await container.read(ContainerPaths.descriptorFile));
  if (descriptor.format !== TestDescriptor.CURRENT_FORMAT) {
    throw new Error(`Invalid descriptor format "${descriptor.format}" (expected ${TestDescriptor.CURRENT_FORMAT})`);
  }
  if (!Array.isArray(descriptor.members)) {
    throw new Error(`CHIP test descriptor has no members`);
  }
  Values.tests = TestDescriptor.Filesystem(descriptor);
}
async function configureNetwork() {
  const accessoryServer = await AccessoryServer.create(State);
  State.onClose(async () => {
    try {
      await accessoryServer.close();
    } catch (e) {
      console.error("Error closing accessory server:", e);
    }
  });
  await State.container.edit(
    edit.sed("s/10.10.10.5/127.0.0.1/g", `s/_PORT = 9000/_PORT = ${accessoryServer.port}/g`),
    ContainerPaths.accessoryClient
  );
}
async function configureLocalController() {
  const dummyScript = '#!/bin/bash\necho "== WebSocket Server Ready"\nexec sleep infinity\n';
  await State.container.exec([
    "bash",
    "-c",
    `echo '${dummyScript}' > /bin/mjs-ws-dummy && chmod +x /bin/mjs-ws-dummy`
  ]);
  Values.localControllerPort = Constants.controllerPort;
}
function loadSubject(factory, kind, appArgs) {
  let forFactory = Values.subjects.get(factory);
  if (forFactory === void 0) {
    Values.subjects.set(factory, forFactory = {});
  }
  let subject = forFactory[kind];
  if (subject === void 0) {
    subject = forFactory[kind] = factory(kind, { appArgs });
  }
  return subject;
}
function storageDirFor(subject) {
  return `"/storage/${subject.id}"`;
}
function createTest(descriptor) {
  if (descriptor.name === void 0) {
    throw new Error(`A CHIP test has no name`);
  }
  if (descriptor.path === void 0) {
    throw new Error(`CHIP test ${descriptor.name} has no path`);
  }
  switch (descriptor.kind) {
    case "yaml":
      return new YamlTest(descriptor, State.container, Values.localControllerPort);
    case "py":
      return new PythonTest(descriptor, State.container, Values.restartFlagHostDir);
    default:
      throw new Error(`Cannot implement CHIP test ${descriptor.name} of kind ${descriptor.kind}`);
  }
}
function formatSha(sha) {
  if (sha.startsWith("sha256:")) {
    sha = sha.substring(7);
  }
  return ansi.bold(sha.substring(0, 12));
}
export {
  State
};
//# sourceMappingURL=state.js.map
