# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# frozen_string_literal: true

require "spec_helper"
require "fileutils"
require "tmpdir"

RSpec.describe PotatoMesh::App::Meshtastic::PayloadDecoder do
  def with_env(key, value)
    previous = ENV[key]
    ENV[key] = value
    yield
  ensure
    ENV[key] = previous
  end

  def with_repo_root(path)
    allow(PotatoMesh::Config).to receive(:repo_root).and_return(path)
  end

  it "prefers a configured python path" do
    Dir.mktmpdir do |dir|
      with_env("MESHTASTIC_PYTHON", "/custom/python") do
        with_repo_root(dir) do
          expect(described_class.python_executable_path).to eq("/custom/python")
        end
      end
    end
  end

  it "uses the project venv when present" do
    Dir.mktmpdir do |dir|
      python_path = File.join(dir, "data", ".venv", "bin", "python")
      FileUtils.mkdir_p(File.dirname(python_path))
      File.write(python_path, "")
      FileUtils.chmod(0o755, python_path)

      with_env("MESHTASTIC_PYTHON", nil) do
        with_repo_root(dir) do
          expect(described_class.python_executable_path).to eq(python_path)
        end
      end
    end
  end

  it "falls back to python on PATH when no venv is available" do
    Dir.mktmpdir do |dir|
      fake_bin = File.join(dir, "bin")
      FileUtils.mkdir_p(fake_bin)
      python_path = File.join(fake_bin, "python3")
      File.write(python_path, "#!/bin/sh\n")
      FileUtils.chmod(0o755, python_path)

      with_env("MESHTASTIC_PYTHON", nil) do
        with_env("PATH", fake_bin) do
          with_repo_root(dir) do
            expect(described_class.python_executable_path).to eq(python_path)
          end
        end
      end
    end
  end

  it "resolves the decoder script path from the repo root" do
    Dir.mktmpdir do |dir|
      script_path = File.join(dir, "data", "mesh_ingestor", "decode_payload.py")
      FileUtils.mkdir_p(File.dirname(script_path))
      File.write(script_path, "")

      with_repo_root(dir) do
        expect(described_class.decoder_script_path).to eq(script_path)
      end
    end
  end

  it "falls back to the web root when the repo root is unavailable" do
    Dir.mktmpdir do |dir|
      script_path = File.join(dir, "data", "mesh_ingestor", "decode_payload.py")
      FileUtils.mkdir_p(File.dirname(script_path))
      File.write(script_path, "")

      with_repo_root(Dir.mktmpdir) do
        allow(PotatoMesh::Config).to receive(:web_root).and_return(dir)
        expect(described_class.decoder_script_path).to eq(script_path)
      end
    end
  end

  it "returns nil when the decoder script is missing" do
    Dir.mktmpdir do |dir|
      with_repo_root(dir) do
        expect(described_class.decoder_script_path).to be_nil
      end
    end
  end

  it "returns nil when the decoder process fails" do
    allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
    allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
    allow(Open3).to receive(:capture3).and_return(["{}", "boom", instance_double(Process::Status, success?: false)])

    expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
  end

  it "returns nil when decoder output is invalid JSON" do
    allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
    allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
    allow(Open3).to receive(:capture3).and_return(["not-json", "", instance_double(Process::Status, success?: true)])

    expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
  end

  it "returns nil when decoder output includes an error" do
    allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
    allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
    allow(Open3).to receive(:capture3).and_return([JSON.generate("error" => "boom"), "", instance_double(Process::Status, success?: true)])

    expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
  end

  it "returns nil when decoder output is not a hash" do
    allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
    allow(described_class).to receive(:python_executable_path).and_return("/usr/bin/python3")
    allow(Open3).to receive(:capture3).and_return([JSON.generate([1, 2, 3]), "", instance_double(Process::Status, success?: true)])

    expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
  end

  it "returns nil when the decoder executable is missing" do
    allow(described_class).to receive(:decoder_script_path).and_return("/tmp/decoder.py")
    allow(described_class).to receive(:python_executable_path).and_return("/missing/python")
    allow(Open3).to receive(:capture3).and_raise(Errno::ENOENT)

    expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
  end

  it "returns nil when decoder paths are unavailable" do
    allow(described_class).to receive(:decoder_script_path).and_return(nil)
    allow(described_class).to receive(:python_executable_path).and_return(nil)

    expect(described_class.decode(portnum: 3, payload_b64: "AA==")).to be_nil
  end

  it "returns nil when no python executable can be found" do
    with_env("MESHTASTIC_PYTHON", nil) do
      with_env("PATH", "") do
        with_repo_root(Dir.mktmpdir) do
          expect(described_class.python_executable_path).to be_nil
        end
      end
    end
  end

  it "returns nil when inputs are missing" do
    expect(described_class.decode(portnum: nil, payload_b64: "AA==")).to be_nil
    expect(described_class.decode(portnum: 3, payload_b64: nil)).to be_nil
  end

  it "falls back to PATH when configured python is blank" do
    Dir.mktmpdir do |dir|
      fake_bin = File.join(dir, "bin")
      FileUtils.mkdir_p(fake_bin)
      python_path = File.join(fake_bin, "python")
      File.write(python_path, "#!/bin/sh\n")
      FileUtils.chmod(0o755, python_path)

      with_env("MESHTASTIC_PYTHON", " ") do
        with_env("PATH", fake_bin) do
          with_repo_root(dir) do
            expect(described_class.python_executable_path).to eq(python_path)
          end
        end
      end
    end
  end
end
