# frozen_string_literal: true

# Copyright (c) [2024-2025] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require_relative "../agama/storage/storage_helpers"
require "agama/config"
require "agama/storage/config"
require "agama/storage/config_conversions"
require "agama/storage/system"
require "y2storage"
require "y2storage/agama_proposal"

describe Y2Storage::AgamaProposal do
  using Y2Storage::Refinements::SizeCasts
  include Agama::RSpec::StorageHelpers

  subject(:proposal) do
    described_class.new(config, storage_system, issues_list: issues_list)
  end

  let(:storage_system) { Agama::Storage::System.new }

  let(:config) { config_from_json }

  let(:config_from_json) do
    Agama::Storage::ConfigConversions::FromJSON
      .new(config_json)
      .convert
  end

  let(:issues_list) { [] }

  before do
    mock_storage(devicegraph: scenario)
    # To speed-up the tests
    allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true)
  end

  let(:scenario) { "disks.yaml" }

  describe "#propose" do
    context "when searching several disks at once and using them as LVM targetDevices" do
      let(:config_json) do
        {
          drives:       [
            { search: { max: 2 }, alias: "first-two" }
          ],
          volumeGroups: [
            {
              name:            "system",
              physicalVolumes: [
                { generate: { targetDevices: ["first-two"] } }
              ],
              logicalVolumes:  [
                {
                  name:       "root",
                  size:       "55 GiB",
                  filesystem: { path: "/" }
                }
              ]
            }
          ]
        }
      end

      it "extends the LVM over all the chosen disks if needed" do
        devicegraph = proposal.propose

        system = devicegraph.find_by_name("/dev/system")
        expect(system.lvm_pvs.map { |pv| pv.blk_device.partitionable.name })
          .to contain_exactly("/dev/vda", "/dev/vdb")
      end
    end

    context "when searching several disks at once and using them as LVM PVs" do
      let(:config_json) do
        {
          drives:       [
            {
              partitions: [
                { search: "/dev/vda1" },
                { search: "*", alias: "rest" }
              ]
            }
          ],
          volumeGroups: [
            {
              name:            "system",
              physicalVolumes: ["rest"],
              logicalVolumes:  [
                {
                  name:       "root",
                  size:       "10 GiB",
                  filesystem: { path: "/" }
                }
              ]
            }
          ]
        }
      end

      it "uses all the disks as physical volumes" do
        probed = Y2Storage::StorageManager.instance.probed
        vda2_sid = probed.find_by_name("/dev/vda2").sid
        vda3_sid = probed.find_by_name("/dev/vda3").sid

        devicegraph = proposal.propose

        system = devicegraph.find_by_name("/dev/system")
        expect(system.lvm_pvs.map(&:blk_device).map(&:sid)).to contain_exactly(vda2_sid, vda3_sid)
      end
    end

    context "when marking several partitions for resizing" do
      let(:config_json) do
        {
          boot:   { configure: false },
          drives: [
            {
              search:     disk_name,
              partitions: [
                { search: search, size: { min: 0, max: "current" } },
                { size: "25 GiB", filesystem: { path: "/" } }
              ]
            }
          ]
        }
      end

      before do
        allow_any_instance_of(Y2Storage::Partition)
          .to(receive(:detect_resize_info))
          .and_return(resize_info)
      end

      let(:resize_info) do
        instance_double(
          Y2Storage::ResizeInfo, resize_ok?: true,
          min_size: Y2Storage::DiskSize::GiB(4), max_size: Y2Storage::DiskSize::GiB(35)
        )
      end

      shared_examples "resize" do
        it "resizes several partitions if needed" do
          probed = Y2Storage::StorageManager.instance.probed
          vda2_size = probed.find_by_name("/dev/vda2").size
          vda3_size = probed.find_by_name("/dev/vda3").size

          devicegraph = proposal.propose

          expect(devicegraph.find_by_name("/dev/vda2").size).to be < vda2_size
          expect(devicegraph.find_by_name("/dev/vda3").size).to be < vda3_size
        end
      end

      context "using an empty search to match the partitions" do
        let(:search) { {} }

        context "if there are several partitions at the disk" do
          let(:disk_name) { "/dev/vda" }

          include_examples "resize"
        end

        context "if there are no partitions in the disk" do
          let(:disk_name) { "/dev/vdc" }

          it "register an error and returns nil" do
            expect(proposal.propose).to be_nil
            expect(proposal.issues_list).to include an_object_having_attributes(
              description: "Mandatory partition not found",
              severity:    Agama::Issue::Severity::ERROR
            )
          end
        end
      end

      context "using asterisk as the search to match the partitions" do
        let(:search) { "*" }

        context "if there are several partitions at the disk" do
          let(:disk_name) { "/dev/vda" }

          include_examples "resize"
        end

        context "if there are no partitions in the disk" do
          let(:disk_name) { "/dev/vdc" }

          it "processes the proposal" do
            devicegraph = proposal.propose
            disk = devicegraph.find_by_name(disk_name)
            expect(disk.partitions.size).to eq 1
          end

          it "does not include any issue about non-existent partitions" do
            proposal.propose
            expect(proposal.issues_list).to be_empty
          end
        end
      end
    end

    context "when searching existing partitions but not specifying any action on them" do
      let(:config_json) do
        {
          boot:   { configure: false },
          drives: [
            {
              search:     "/dev/vda",
              partitions: [
                { search: "*" },
                { size: "20 GiB", filesystem: { path: "/" } }
              ]
            }
          ]
        }
      end

      # Regression test. In the past the proposal tried to resize some partitions due to the
      # search "*" without actions.
      it "no partitions are deleted or resized" do
        expect_any_instance_of(Y2Storage::Partition).to_not receive(:detect_resize_info)
        # There is no way to make 20 GiB fit without resizing or deleting
        expect { proposal.propose }.to raise_error(Y2Storage::NoDiskSpaceError)
      end
    end
  end
end
