const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { EventEmitter } = require("events");
const { PassThrough } = require("stream");
const {
  DevelopmentUpdateService
} = require("../src/services/development-updates");
const {
  isStrictLoopbackRequest,
  resolveRuntimeEnvironment
} = require("../src/services/runtime-environment");
const {
  sameLocalhostOrigin
} = require("../plugins/lumi_transcription/backend/companion/device_store");

async function main() {
  const companionManifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "plugins", "lumi_transcription", "companion_manifest.json"), "utf8"));
  const transcriptionManifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "plugins", "lumi_transcription", "plugin.json"), "utf8"));
  const companionProject = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "Lumi.Companion.App.csproj"), "utf8");
  const installerSource = fs.readFileSync(path.join(__dirname, "..", "companion", "installer", "Lumi.Companion.iss"), "utf8");
  const publisherSource = fs.readFileSync(path.join(__dirname, "..", "companion", "scripts", "publish-companion.ps1"), "utf8");
  const companionAppSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "App.axaml.cs"), "utf8");
  const iconFactorySource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "LumiIconFactory.cs"), "utf8");
  const iconPath = path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "Assets", "Lumi.Companion.ico");
  const projectVersion = /<Version>([^<]+)<\/Version>/.exec(companionProject)?.[1];
  const installerVersion = /#define AppVersion "([^"]+)"/.exec(installerSource)?.[1];
  const publisherVersion = /\[string\]\$Version = "([^"]+)"/.exec(publisherSource)?.[1];
  assert.equal(companionManifest.version, projectVersion, "the published Companion manifest and executable version must match");
  assert.equal(transcriptionManifest.version, projectVersion, "the transcription plugin and Companion release version must match");
  assert.equal(installerVersion, projectVersion, "the installer default version must match the Companion executable");
  assert.equal(publisherVersion, projectVersion, "the release publisher default version must match the Companion executable");
  assert(companionProject.includes("<ApplicationIcon>Assets\\Lumi.Companion.ico</ApplicationIcon>"));
  assert(companionProject.includes('<AvaloniaResource Include="Assets\\Lumi.Companion.ico"'));
  assert.deepEqual([...fs.readFileSync(iconPath).subarray(0, 4)], [0, 0, 1, 0], "the Companion Windows icon must be a valid ICO resource");
  assert(iconFactorySource.includes("Icons.TryGetValue(health"), "tray health icons must be cached instead of recreated for every state event");
  assert(companionAppSource.includes("if (state.Health != renderedHealth)"), "the native tray icon must only be assigned when its color state changes");
  assert(companionAppSource.includes("CompanionState? pendingState"), "rapid tray state notifications must be coalesced on the UI thread");
  for (const [directive, filename] of [
    ["LicenseFile", "LUMI-COMPANION-LICENCE.txt"],
    ["InfoBeforeFile", "PRIVACY-NOTICE.txt"],
    ["InfoAfterFile", "THIRD-PARTY-NOTICES.txt"]
  ]) {
    assert(
      installerSource.includes(`${directive}={#SourceRoot}\\legal\\${filename}`),
      `${filename} must be presented by the Companion installer`
    );
    assert(fs.existsSync(path.join(__dirname, "..", "companion", "legal", filename)), `${filename} must exist`);
  }

  const temp = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-dev-updates-"));
  try {
    fs.mkdirSync(path.join(temp, ".git"));
    fs.mkdirSync(path.join(temp, "companion", "src"), { recursive: true });
    fs.mkdirSync(path.join(temp, "companion", "legal"), { recursive: true });
    fs.mkdirSync(path.join(temp, "companion", "scripts"), { recursive: true });
    fs.mkdirSync(path.join(temp, "companion", "plugins", "Song"), { recursive: true });
    fs.mkdirSync(path.join(temp, "companion", "plugins", "Transcript"), { recursive: true });
    fs.mkdirSync(path.join(temp, "plugins", "now_playing"), { recursive: true });
    fs.writeFileSync(path.join(temp, "companion", "Lumi.Companion.sln"), "solution");
    fs.writeFileSync(path.join(temp, "companion", "src", "App.cs"), "core-a");
    fs.writeFileSync(path.join(temp, "companion", "legal", "notice.txt"), "notice-a");
    fs.writeFileSync(path.join(temp, "companion", "scripts", "publish-dev-update.ps1"), "# publisher-a");
    fs.writeFileSync(path.join(temp, "companion", "plugins", "Song", "Plugin.cs"), "song-a");
    fs.writeFileSync(path.join(temp, "companion", "plugins", "Song", "plugin.json"), JSON.stringify({ id: "now_playing" }));
    fs.writeFileSync(path.join(temp, "companion", "plugins", "Transcript", "Plugin.cs"), "transcript-a");
    fs.writeFileSync(path.join(temp, "plugins", "now_playing", "index.js"), "server-a");

    const sourceMode = resolveRuntimeEnvironment({ repoRoot: temp, env: {} });
    assert.equal(sourceMode.isDevelopment, true);
    assert.equal(resolveRuntimeEnvironment({ repoRoot: temp, env: { NODE_ENV: "production" } }).isProduction, true);
    assert.equal(resolveRuntimeEnvironment({ repoRoot: temp, env: { LUMI_DEV_MODE: "1", NODE_ENV: "production" } }).isDevelopment, true);
    assert.equal(resolveRuntimeEnvironment({ repoRoot: temp, env: { LUMI_DEV_MODE: "0" } }).isProduction, true);
    assert.equal(isStrictLoopbackRequest({ hostname: "localhost", socket: { remoteAddress: "127.0.0.1" } }), true);
    assert.equal(isStrictLoopbackRequest({ hostname: "localhost", socket: { remoteAddress: "192.168.1.50" } }), false);
    assert.equal(isStrictLoopbackRequest({ hostname: "lumi.local", socket: { remoteAddress: "127.0.0.1" } }), false);
    assert.equal(sameLocalhostOrigin("http://localhost:3000", "http://localhost:3000"), true);
    assert.equal(sameLocalhostOrigin("https://localhost:3443", "https://localhost:3443"), true);
    assert.equal(sameLocalhostOrigin("http://localhost:3000", "http://127.0.0.1:3000"), false);

    const service = new DevelopmentUpdateService({ repoRoot: temp, cacheRoot: path.join(temp, "data", "development-updates"), runtime: sourceMode });
    const first = service.inventory();
    assert(first.components.some((entry) => entry.id === "companion:core"));
    assert(
      first.components.find((entry) => entry.id === "companion:core").paths.includes("companion/scripts/publish-dev-update.ps1"),
      "changes to the localhost publisher must invalidate the cached Companion artifact"
    );
    assert(first.components.some((entry) => entry.id === "companion-plugin:Song"));
    assert(first.components.some((entry) => entry.id === "lumi-plugin:now_playing"));

    const firstMap = Object.fromEntries(first.updatable_components.map((entry) => [entry.id, entry.checksum]));
    assert.equal(service.compare({ build_checksum: first.aggregate_checksum, component_checksums: firstMap }).update_available, false);

    fs.writeFileSync(path.join(temp, "companion", "plugins", "Song", "Plugin.cs"), "song-b");
    const second = service.compare({ build_checksum: first.aggregate_checksum, component_checksums: firstMap });
    assert.equal(second.update_available, true);
    assert.deepEqual(second.changed_components.map((entry) => entry.id), ["companion-plugin:Song"]);

    fs.mkdirSync(path.join(temp, "companion", "plugins", "Song", "bin"), { recursive: true });
    fs.writeFileSync(path.join(temp, "companion", "plugins", "Song", "bin", "ignored.dll"), "volatile");
    const third = service.inventory();
    assert.equal(third.aggregate_checksum, second.aggregate_checksum);

    const beforeServerChange = third.aggregate_checksum;
    const beforeServerChecksum = third.components.find((entry) => entry.id === "lumi-plugin:now_playing").checksum;
    fs.writeFileSync(path.join(temp, "plugins", "now_playing", "index.js"), "server-b");
    const fourth = service.compare({
      build_checksum: beforeServerChange,
      component_checksums: Object.fromEntries(third.tracked_components.map((entry) => [entry.id, entry.checksum]))
    });
    assert.notEqual(fourth.aggregate_checksum, beforeServerChange, "a Companion-related Lumi plugin change must be surfaced to the Companion");
    assert.notEqual(fourth.components.find((entry) => entry.id === "lumi-plugin:now_playing").checksum, beforeServerChecksum);
    assert(fourth.changed_components.some((entry) => entry.id === "lumi-plugin:now_playing"));

    const fakeSpawn = (_command, args) => {
      const child = new EventEmitter();
      child.stdout = new PassThrough();
      child.stderr = new PassThrough();
      child.kill = () => child.emit("exit", 1);
      setImmediate(() => {
        const outputIndex = args.indexOf("-OutputArchive");
        const output = args[outputIndex + 1];
        fs.mkdirSync(path.dirname(output), { recursive: true });
        fs.writeFileSync(output, "fake-local-development-archive");
        child.emit("exit", 0);
      });
      return child;
    };
    const buildService = new DevelopmentUpdateService({
      repoRoot: temp,
      cacheRoot: path.join(temp, "data", "development-updates"),
      runtime: sourceMode,
      spawn: fakeSpawn,
      platform: "win32"
    });
    const built = await buildService.buildCompanionArtifact(fourth);
    assert.equal(built.build_id, fourth.aggregate_checksum);
    assert.equal(buildService.readArtifact(built.build_id).sha256, built.sha256);
    assert.equal((await buildService.buildCompanionArtifact(fourth)).path, built.path, "the checksum-addressed build should be reused");

    const serviceSource = fs.readFileSync(path.join(__dirname, "..", "src", "services", "development-updates.js"), "utf8");
    assert(!/exec(?:File|Sync)?\([^\n]*git\b/i.test(serviceSource), "local development updates must not invoke Git");
    const transcriptionSource = fs.readFileSync(path.join(__dirname, "..", "plugins", "lumi_transcription", "index.js"), "utf8");
    assert(transcriptionSource.includes("allowsLocalDevelopmentUpdates"));
    assert(transcriptionSource.includes("dev-artifact/:buildId"));
    const updateSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "UpdateService.cs"), "utf8");
    assert(updateSource.includes("DevelopmentBuildManifest.Load()"));
    assert(updateSource.includes("update.Development"));
    assert(updateSource.includes("SameOrigin"));
    assert(updateSource.includes("SupportsPendingBuild"));
    assert(updateSource.includes("BuildPending"));
    assert(transcriptionSource.includes("supports_pending_build"));
    assert(transcriptionSource.includes("build_pending"));
    const applierSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "UpdateApplier.cs"), "utf8");
    assert(applierSource.includes(".lumi-dev-build.json"));

    console.log("Local development update verification passed: global runtime mode, strict loopback gating, per-plugin checksums, ignored build output, Companion-related server-plugin synchronization, Git-free comparison, and Companion same-version update flow.");
  } finally {
    fs.rmSync(temp, { recursive: true, force: true });
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
