# typed: true
# frozen_string_literal: true

require "livecheck/strategy"
require "rexml/document"
require "rexml/undefinednamespaceexception"

RSpec.describe Homebrew::Livecheck::Strategy::Xml do
  subject(:xml) { described_class }

  let(:http_url) { "https://brew.sh/blog/" }
  let(:non_http_url) { "ftp://brew.sh/" }
  let(:regex) { /^v?(\d+(?:\.\d+)+)$/i }
  let(:content_version_text) do
    <<~XML
      <?xml version="1.0" encoding="utf-8"?>
      <versions>
        <version>1.1.2</version>
        <version>1.1.2b</version>
        <version>1.1.2a</version>
        <version>1.1.1</version>
        <version>1.1.0</version>
        <version>1.1.0-rc3</version>
        <version>1.1.0-rc2</version>
        <version>1.1.0-rc1</version>
        <version>1.0.x-last</version>
        <version>1.0.3</version>
        <version>1.0.3-rc3</version>
        <version>1.0.3-rc2</version>
        <version>1.0.3-rc1</version>
        <version>1.0.2</version>
        <version>1.0.2-rc1</version>
        <version>1.0.1</version>
        <version>1.0.1-rc1</version>
        <version>1.0.0</version>
        <version>1.0.0-rc1</version>
      </versions>
    XML
  end
  let(:content_version_attr) do
    <<~XML
      <?xml version="1.0" encoding="utf-8"?>
      <items>
        <item version="1.1.2" />
        <item version="1.1.2b" />
        <item version="1.1.2a" />
        <item version="1.1.1" />
        <item version="1.1.0" />
        <item version="1.1.0-rc3" />
        <item version="1.1.0-rc2" />
        <item version="1.1.0-rc1" />
        <item version="1.0.x-last" />
        <item version="1.0.3" />
        <item version="1.0.3-rc3" />
        <item version="1.0.3-rc2" />
        <item version="1.0.3-rc1" />
        <item version="1.0.2" />
        <item version="1.0.2-rc1" />
        <item version="1.0.1" />
        <item version="1.0.1-rc1" />
        <item version="1.0.0" />
        <item version="1.0.0-rc1" />
      </items>
    XML
  end
  let(:content_simple) do
    <<~XML
      <?xml version="1.0" encoding="utf-8"?>
      <version>1.2.3</version>
    XML
  end
  let(:content_undefined_namespace) do
    <<~XML
      <?xml version="1.0" encoding="utf-8"?>
      <something:version>1.2.3</something:version>
    XML
  end
  let(:parent_child_text) { { parent: "1.2.3", child: "4.5.6" } }
  let(:content_parent_child) do
    # This XML deliberately includes unnecessary whitespace, to ensure that
    # Xml#element_text properly strips the retrieved text.
    <<~XML
      <?xml version="1.0" encoding="utf-8"?>
      <elements>
        <parent>
          #{parent_child_text[:parent]}
          <child> #{parent_child_text[:child]} </child>
        </parent>
        <blank-parent>
          <blank-child></blank-child>
        </blank-parent>
      </elements>
    XML
  end
  let(:matches) do
    {
      content: ["1.1.2", "1.1.1", "1.1.0", "1.0.3", "1.0.2", "1.0.1", "1.0.0"],
      simple:  ["1.2.3"],
    }
  end

  describe "::match?" do
    it "returns true for an HTTP URL" do
      expect(xml.match?(http_url)).to be true
    end

    it "returns false for a non-HTTP URL" do
      expect(xml.match?(non_http_url)).to be false
    end
  end

  describe "::parse_xml" do
    # TODO: Should we be comparing against an actual REXML::Document object?
    it "returns an REXML::Document when given XML content" do
      expect(xml.parse_xml(content_version_text)).to be_an_instance_of(REXML::Document)
    end

    it "returns an REXML::Document when given XML content with an undefined namespace" do
      expect(xml.parse_xml(content_undefined_namespace)).to be_an_instance_of(REXML::Document)
    end

    it "errors if an undefined prefix name is not provided in the UndefinedNamespaceException" do
      allow(REXML::Document).to receive(:new).and_raise(REXML::UndefinedNamespaceException.new(nil, nil, nil))

      expect { xml.parse_xml(content_undefined_namespace) }
        .to raise_error("Could not identify undefined prefix.")
    end

    it "errors if XML cannot be parsed after removing undefined prefix" do
      allow(REXML::Document).to receive(:new).and_raise(REXML::UndefinedNamespaceException.new("something", nil, nil))

      expect { xml.parse_xml(content_undefined_namespace) }
        .to raise_error("Could not parse XML after removing undefined prefix.")
    end
  end

  describe "::element_text" do
    let(:parent_child_doc) { xml.parse_xml(content_parent_child) }
    let(:parent) { parent_child_doc.get_elements("/elements/parent").first }
    let(:blank_parent) { parent_child_doc.get_elements("/elements/blank-parent").first }

    it "returns the element text if child_name is not provided" do
      expect(xml.element_text(parent)).to eq(parent_child_text[:parent])
    end

    it "returns the child element text if child_name is provided" do
      expect(xml.element_text(parent, "child")).to eq(parent_child_text[:child])
    end

    it "returns `nil` if the provided child element does not exist" do
      expect(xml.element_text(parent, "nonexistent")).to be_nil
    end

    it "returns `nil` if the retrieved text is blank" do
      expect(xml.element_text(blank_parent)).to be_nil
      expect(xml.element_text(blank_parent, "blank-child")).to be_nil
    end
  end

  describe "::versions_from_content" do
    it "returns an empty array when given a block but content is blank" do
      expect(xml.versions_from_content("", regex) { "1.2.3" }).to eq([])
    end

    it "returns an array of version strings when given content and a block" do
      # Returning a string from block
      expect(xml.versions_from_content(content_simple) do |xml|
        xml.elements["version"]&.text
      end).to eq(matches[:simple])
      expect(xml.versions_from_content(content_simple, regex) do |xml|
        version = xml.elements["version"]&.text
        next if version.blank?

        version[regex, 1]
      end).to eq(matches[:simple])

      # Returning an array of strings from block
      expect(xml.versions_from_content(content_version_text, regex) do |xml, regex|
        xml.get_elements("/versions/version").map { |item| item.text[regex, 1] }
      end).to eq(matches[:content])

      expect(xml.versions_from_content(content_version_attr, regex) do |xml, regex|
        xml.get_elements("/items/item").map do |item|
          version = item["version"]
          next if version.blank?

          version[regex, 1]
        end
      end).to eq(matches[:content])
    end

    it "allows a nil return from a block" do
      expect(xml.versions_from_content(content_simple, regex) { next }).to eq([])
    end

    it "errors if a block uses two arguments but a regex is not given" do
      expect { xml.versions_from_content(content_simple) { |xml, regex| xml["version"][regex, 1] } }
        .to raise_error("Two arguments found in `strategy` block but no regex provided.")
    end

    it "errors on an invalid return type from a block" do
      expect { xml.versions_from_content(content_simple, regex) { 123 } }
        .to raise_error(TypeError, Homebrew::Livecheck::Strategy::INVALID_BLOCK_RETURN_VALUE_MSG)
    end
  end

  describe "::find_versions" do
    let(:match_data) do
      base = {
        matches: matches[:content].to_h { |v| [v, Version.new(v)] },
        regex:,
        url:     http_url,
      }

      {
        fetched:        base.merge({ content: content_version_text }),
        cached:         base.merge({ cached: true }),
        cached_default: base.merge({ matches: {}, cached: true }),
      }
    end

    it "finds versions in fetched content" do
      allow(Homebrew::Livecheck::Strategy).to receive(:page_content).and_return({ content: content_version_text })

      expect(xml.find_versions(url: http_url, regex:) do |xml, regex|
        xml.get_elements("/versions/version").map { |item| item.text[regex, 1] }
      end).to eq(match_data[:fetched])
    end

    it "finds versions in content using a block" do
      expect(xml.find_versions(url: http_url, regex:, content: content_version_text) do |xml, regex|
        xml.get_elements("/versions/version").map { |item| item.text[regex, 1] }
      end).to eq(match_data[:cached])

      # NOTE: A regex should be provided using the `#regex` method in a
      #       `livecheck` block but we're using a regex literal in the
      #       `strategy` block here simply to ensure this method works as
      #       expected when a regex isn't provided.
      expect(xml.find_versions(url: http_url, content: content_version_text) do |xml|
        regex = /^v?(\d+(?:\.\d+)+)$/i
        xml.get_elements("/versions/version").map { |item| item.text[regex, 1] }
      end).to eq(match_data[:cached].merge({ regex: nil }))
    end

    it "errors if a block is not provided" do
      expect { xml.find_versions(url: http_url, content: content_simple) }
        .to raise_error(ArgumentError, "Xml requires a `strategy` block")
    end

    it "returns default match_data when url is blank" do
      expect(xml.find_versions(url: "", regex:, content: content_simple) { "1.2.3" })
        .to eq(match_data[:cached_default].merge({ url: "" }))
    end

    it "returns default match_data when content is blank" do
      expect(xml.find_versions(url: http_url, regex:, content: "") { "1.2.3" })
        .to eq(match_data[:cached_default])
    end
  end
end
