require "rails_helper"

RSpec.describe "Build configs endpoint", type: :request do
  let(:signing_key) { OpenSSL::PKey.generate_key("ed25519") }
  let(:ci_platforms) do
    {
      "godot" => { builder: "registry.example/godot-builder:4.6" },
      "tic80" => { builder: "registry.example/tic80-builder:1.0",
                   exporter: "registry.example/tic80pro:1.0" }
    }
  end

  before do
    allow(WarpEngine.config).to receive(:ci_platforms).and_return(ci_platforms)
    allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(signing_key.public_to_pem)
  end

  # Woodpecker 3.x-style RFC 9421 signature over @request-target + content-digest.
  def signed_headers(body, path: "/build/config", digest_body: nil)
    digest = "sha-256=:#{Digest::SHA256.base64digest(digest_body || body)}:"
    inner = %{("@request-target" "content-digest");created=#{Time.now.to_i};alg="ed25519"}
    base = [
      %("@request-target": #{path}),
      %("content-digest": #{digest}),
      %("@signature-params": #{inner})
    ].join("\n")
    signature = Base64.strict_encode64(signing_key.sign(nil, base))
    {
      "Content-Digest" => digest,
      "Signature-Input" => "woodpecker-ci-extensions=#{inner}",
      "Signature" => "woodpecker-ci-extensions=:#{signature}:",
      "Content-Type" => "application/json"
    }
  end

  # Legacy draft-cavage signature (single Signature header).
  def cavage_signed_headers(method: "post", path: "/build/config")
    date = Time.now.httpdate
    signing_string = "(request-target): #{method} #{path}\ndate: #{date}"
    signature = Base64.strict_encode64(signing_key.sign(nil, signing_string))
    {
      "Date" => date,
      "Signature" => %(keyId="woodpecker-ci-plugins",algorithm="ed25519",headers="(request-target) date",signature="#{signature}"),
      "Content-Type" => "application/json"
    }
  end

  def extension_payload(marker_yaml, repo_name: "mygame")
    {
      repo: { name: repo_name },
      pipeline: { branch: "master" },
      configuration: [ { name: ".woodpecker.yaml", data: marker_yaml } ]
    }.to_json
  end

  describe "GET /build/config" do
    it "renders the pipeline for a configured platform" do
      get "/build/config", params: { platform: "godot", name: "mygame" }

      expect(response).to have_http_status(:ok)
      pipeline = YAML.safe_load(response.body)
      expect(pipeline["steps"]).to be_present
      expect(response.body).to include("registry.example/godot-builder:4.6")
      expect(response.body).to include("mygame")
    end

    it "returns 404 for an unknown platform" do
      get "/build/config", params: { platform: "nope" }

      expect(response).to have_http_status(:not_found)
    end

    it "returns 404 when the feature is not configured" do
      allow(WarpEngine.config).to receive(:ci_platforms).and_return({})

      get "/build/config", params: { platform: "godot" }

      expect(response).to have_http_status(:not_found)
    end

    it "rejects path traversal in the platform param" do
      get "/build/config", params: { platform: "../secrets" }

      expect(response).to have_http_status(:not_found)
    end
  end

  describe "POST /build/config" do
    it "returns the rendered pipeline for a marker config" do
      payload = extension_payload("platform: godot\n")
      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response).to have_http_status(:ok)
      configs = response.parsed_body["configs"]
      expect(configs.length).to eq(1)
      expect(configs.first["name"]).to eq("godot")
      pipeline = YAML.safe_load(configs.first["data"])
      expect(pipeline["steps"].map { |s| s["name"] }).to include("version", "publish")
      expect(configs.first["data"]).to include("mygame")
    end

    it "uses the marker's name override instead of the repo name" do
      payload = extension_payload("platform: godot\nname: othername\n")
      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response.parsed_body["configs"].first["data"]).to include("othername")
      expect(response.parsed_body["configs"].first["data"]).not_to include("mygame")
    end

    it "accepts the configs key used by older Woodpecker payloads" do
      payload = { repo: { name: "mygame" },
                  configs: [ { name: ".woodpecker.yaml", data: "platform: godot\n" } ] }.to_json

      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response).to have_http_status(:ok)
    end

    it "accepts a legacy draft-cavage signed request" do
      payload = extension_payload("platform: godot\n")
      post "/build/config", params: payload, headers: cavage_signed_headers

      expect(response).to have_http_status(:ok)
    end

    it "returns 204 for a non-marker config" do
      payload = extension_payload("steps:\n  - name: build\n    image: alpine\n")
      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response).to have_http_status(:no_content)
    end

    it "returns 204 when no configuration is sent" do
      payload = { repo: { name: "mygame" } }.to_json
      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response).to have_http_status(:no_content)
    end

    it "returns 422 for a marker with an unknown platform" do
      payload = extension_payload("platform: amiga\n")
      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response).to have_http_status(:unprocessable_entity)
    end

    it "rejects a request with an invalid signature" do
      payload = extension_payload("platform: godot\n")
      headers = signed_headers(payload)
      other_key = OpenSSL::PKey.generate_key("ed25519")
      allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(other_key.public_to_pem)

      post "/build/config", params: payload, headers: headers

      expect(response).to have_http_status(:forbidden)
    end

    it "rejects a request whose body does not match the signed content-digest" do
      payload = extension_payload("platform: godot\n")
      tampered = signed_headers(payload, digest_body: "something else")

      post "/build/config", params: payload, headers: tampered

      expect(response).to have_http_status(:forbidden)
    end

    it "rejects a request without a signature header" do
      post "/build/config", params: extension_payload("platform: godot\n"),
                            headers: { "Content-Type" => "application/json" }

      expect(response).to have_http_status(:forbidden)
    end

    it "rejects every request when no public key is configured" do
      allow(WarpEngine.config).to receive(:ci_extension_public_key).and_return(nil)
      allow(WarpEngine.config).to receive(:ci_extension_public_key_url).and_return(nil)

      payload = extension_payload("platform: godot\n")
      post "/build/config", params: payload, headers: signed_headers(payload)

      expect(response).to have_http_status(:forbidden)
    end
  end

  describe "shipped templates" do
    it "renders every template to valid YAML with non-empty steps" do
      templates = Dir[WarpEngine::Engine.root.join("app/services/warp_engine/platforms/*/pipeline.yaml.erb")]
      expect(templates).not_to be_empty

      templates.each do |path|
        platform = File.basename(File.dirname(path))
        allow(WarpEngine.config).to receive(:ci_platforms).and_return(
          platform => { builder: "registry.example/builder:1", exporter: "registry.example/exporter:1" }
        )

        yaml = WarpEngine::CiConfigService.new.render(
          platform: platform, name: "example", update_server: "https://games.example"
        )

        expect(yaml).to be_present, "#{platform}: no template rendered"
        pipeline = YAML.safe_load(yaml)
        expect(pipeline["steps"]).to be_present, "#{platform}: no steps"
        pipeline["steps"].each do |step|
          expect(step["image"]).to be_present, "#{platform}/#{step['name']}: missing image"
          expect(step["commands"]).to be_present, "#{platform}/#{step['name']}: missing commands"
        end
      end
    end
  end
end
