#  Copyright (c) 2018-2022, Grünliberale Partei Schweiz. This file is part o
#  hitobito and licensed under the Affero General Public License version 3
#  or later. See the COPYING file at the top-level directory or at
#  https://github.com/hitobito/hitobito.

require "spec_helper"

describe Synchronize::Mailchimp::Client do
  let(:mailing_list) {
    MailingList.new(mailchimp_api_key: "1234567890d66d25cc5c9285ab5a5552-us12", mailchimp_list_id: 2)
  }
  let(:top_leader) { people(:top_leader) }
  let(:client) { described_class.new(mailing_list) }

  def stub_collection(path, offset, count = client.count, body:)
    stub_request(:get, "https://us12.api.mailchimp.com/3.0/#{path}?count=#{count}&offset=#{offset}")
      .with(
        headers: {
          "Accept" => "*/*",
          "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
          "Authorization" => "Basic YXBpa2V5OjEyMzQ1Njc4OTBkNjZkMjVjYzVjOTI4NWFiNWE1NTUyLXVzMTI=",
          "Content-Type" => "application/json"
        }
      )
      .to_return(status: 200, body: body.to_json, headers: {})
  end

  def create_tgz(*payloads)
    Dir.mktmpdir do |dir|
      payloads.reverse.each_with_index do |payload, index|
        Pathname(dir).join("payload-#{index}.json").write(payload.to_json)
      end
      Open3.pipeline_r("tar -zcf - -C #{dir} .").first.read
    end
  end

  def stub_merge_fields(*fields, total_items: nil, offset: 0)
    entries = fields.collect do |tag, name, type|
      {tag: tag, name: name, type: type}
    end
    stub_collection("lists/2/merge-fields", offset,
      body: {merge_fields: entries, total_items: total_items || entries.count})
  end

  def stub_members(*members, total_items: nil, offset: 0)
    stub_collection("lists/2/members", offset, body: build_members_response_body(*members, total_items:))
  end

  def build_members_response_body(*members, total_items: nil)
    entries = members.collect do |email, status = "subscribed", tags = [], merge_fields = {}, extra_fields = {}|
      {email_address: email, status: status, tags: tags, merge_fields: merge_fields}.merge(extra_fields)
    end
    {members: entries, total_items: total_items || entries.count}
  end

  def stub_segments(*segments, total_items: nil, offset: 0)
    entries = segments.collect do |name, id|
      {name: name, id: id.to_i}
    end
    stub_collection("lists/2/segments", offset, body: {segments: entries, total_items: total_items || entries.count})
  end

  context "#subscriber_body" do
    it "strips whitespace from fields" do
      top_leader.update_columns(first_name: " top ", last_name: " leader ", email: " top@example.com ")
      body = client.subscriber_body(top_leader)

      expect(body[:email_address]).to eq "top@example.com"
      expect(body[:merge_fields][:FNAME]).to eq "top"
      expect(body[:merge_fields][:LNAME]).to eq "leader"
    end

    it "handles nil values" do
      top_leader.update(first_name: nil, last_name: nil, email: " top@example.com ")
      body = client.subscriber_body(top_leader)

      expect(body[:email_address]).to eq "top@example.com"
      expect(body[:merge_fields][:FNAME]).to eq ""
      expect(body[:merge_fields][:LNAME]).to eq ""
    end

    context "merge_fields" do
      let(:merge_field) {
        ["Gender", "dropdown", {choices: %w[m]}, ->(p) { p.gender }]
      }
      let(:client) { described_class.new(mailing_list, merge_fields: [merge_field]) }

      it "excludes blank value" do
        body = client.subscriber_body(top_leader)
        expect(body[:merge_fields]).not_to have_key :GENDER
      end

      it "excludes present value that is not part of choices" do
        top_leader.update(gender: "w")
        body = client.subscriber_body(top_leader)
        expect(body[:merge_fields]).not_to have_key :GENDER
      end

      it "includes present value" do
        top_leader.update(gender: "m")
        body = client.subscriber_body(top_leader)
        expect(body[:merge_fields][:GENDER]).to eq "m"
      end
    end

    context "member_fields" do
      let(:member_field) {
        [:company, ->(p) { p.company }]
      }
      let(:client) { described_class.new(mailing_list, member_fields: [member_field]) }

      it "excludes blank value" do
        body = client.subscriber_body(top_leader)
        expect(body).not_to have_key :company
      end

      it "includes present value" do
        top_leader.update!(company: true, company_name: "acme")
        body = client.subscriber_body(top_leader)
        expect(body[:company]).to eq true
      end
    end
  end

  context "#merge_fields" do
    subject { client.fetch_merge_fields }

    it "returns empty merge_fields list" do
      stub_merge_fields
      expect(subject).to be_empty
    end

    it "returns merge_fields with id" do
      stub_merge_fields(["FNAME", "First Name", "text"],
        ["GENDER", "Gender", "text"])
      expect(subject).to have(2).items

      first = subject.first
      expect(first[:tag]).to eq "FNAME"
      expect(first[:name]).to eq "First Name"
      expect(first[:type]).to eq "text"

      second = subject.second
      expect(second[:tag]).to eq "GENDER"
      expect(second[:name]).to eq "Gender"
      expect(second[:type]).to eq "text"
    end
  end

  context "#segments" do
    subject { client.fetch_segments }

    it "returns empty segment list" do
      stub_segments
      expect(subject).to be_empty
    end

    it "returns segments with id" do
      stub_segments(%w[a 1], %w[b 2])
      expect(subject).to have(2).items

      first = subject.first
      expect(first[:name]).to eq "a"
      expect(first[:id]).to eq 1

      second = subject.second
      expect(second[:name]).to eq "b"
      expect(second[:id]).to eq 2
    end
  end

  context "#members" do
    subject { client.fetch_members }

    it "returns empty member list" do
      stub_members
      expect(subject).to be_empty
    end

    it "returns members with subscription state" do
      stub_members(%w[a@example.com subscribed], %w[b@example.com unsubscribed])
      expect(subject).to have(2).items

      first = subject.first
      expect(first[:email_address]).to eq "a@example.com"
      expect(first[:status]).to eq "subscribed"

      second = subject.second
      expect(second[:email_address]).to eq "b@example.com"
      expect(second[:status]).to eq "unsubscribed"
    end

    it "returns members with downcased emails" do
      stub_members(%w[A@EXAMPLE.COM subscribed])
      expect(subject.first[:email_address]).to eq "a@example.com"
    end

    it "returns members with tags" do
      stub_members(["a@example.com", nil, [{id: 1, name: "test:ab"}, {id: 2, name: "test"}]])
      expect(subject.first[:tags]).to eq [{id: 1, name: "test:ab"}, {id: 2, name: "test"}]
    end

    it "returns members with merge fields" do
      stub_members(["a@example.com", nil, nil, {FNAME: "A", LNAME: "B", GENDER: "m"}])
      expect(subject.first[:merge_fields]).to eq({FNAME: "A", LNAME: "B", GENDER: "m"})
    end

    it "returns members with custom member fields" do
      expect(client).to receive(:member_fields).and_return([["company"]])
      stub_members(["a@example.com", nil, nil, {}, {company: :acme}])
      expect(subject.first[:company]).to eq "acme"
    end

    context "paging" do
      let(:client) { described_class.new(mailing_list, count: 2) }

      it "fetches until total has been reached" do
        stub_members(%w[a@example.com], %w[b@example.com], total_items: 5)
        stub_members(%w[c@example.com], %w[d@example.com], total_items: 5, offset: 2)
        stub_members(%w[e@example.com], total_items: 5, offset: 4)
        expect(subject).to have(5).items

        users = subject.collect { |e| e[:email_address].split("@").first }
        expect(users).to eq %w[a b c d e]
      end

      it "succeeds with retries" do
        stub_members(%w[a@example.com], %w[b@example.com], total_items: 5)
        stub_members(%w[c@example.com], %w[d@example.com], total_items: 5, offset: 2)
        stub_request(:get, "https://us12.api.mailchimp.com/3.0/lists/2/members?count=2&offset=4")
          .to_timeout
          .and_return(
            {status: 400},
            {status: 400},
            {status: 200, body: build_members_response_body(%w[e@example.com]).to_json}
          )

        expect(subject).to have(5).items

        users = subject.collect { |e| e[:email_address].split("@").first }
        expect(users).to eq %w[a b c d e]
      end

      it "fails if max retries are exceeded" do
        stub_members(%w[a@example.com], %w[b@example.com], total_items: 5)
        stub_members(%w[c@example.com], %w[d@example.com], total_items: 5, offset: 2)
        stub_request(:get, "https://us12.api.mailchimp.com/3.0/lists/2/members?count=2&offset=4")
          .and_return(
            {status: 400},
            {status: 400},
            {status: 400},
            {status: 400},
            {status: 400}
          )
        expect { client.fetch_members }.to raise_error(RuntimeError, /Max retries exceeded/)
      end

      it "fails for other than 400 status codes" do
        stub_members(%w[a@example.com], %w[b@example.com], total_items: 5)
        stub_members(%w[c@example.com], %w[d@example.com], total_items: 5, offset: 2)
        stub_request(:get, "https://us12.api.mailchimp.com/3.0/lists/2/members?count=2&offset=4")
          .and_return(
            {status: 401}
          )
        expect { client.fetch_members }.to raise_error(Gibbon::MailChimpError)
      end
    end
  end

  context "#ping_api" do
    it "return true on success" do
      expect(client).to receive_message_chain("api.ping.retrieve").and_return true

      expect(client.ping_api(error_handler: nil)).to eq true
    end

    it "calls error handler with exception" do
      expect(client).to receive_message_chain("api.ping.retrieve").and_raise(Gibbon::MailChimpError, "invalid api key")

      received_exception = nil
      client.ping_api(error_handler: lambda { |e| received_exception = e })

      expect(received_exception.message).to include "invalid api key"
    end
  end

  context "#create_merge_field_operation" do
    subject { client.create_merge_field_operation("Gender", "dropdown", {choices: %w[m w]}) }

    it "POSTs to segments list resource" do
      expect(subject[:method]).to eq "POST"
      expect(subject[:path]).to eq "lists/2/merge-fields"
    end

    it "body includes name and static_segment fields" do
      body = JSON.parse(subject[:body])
      expect(body["tag"]).to eq "GENDER"
      expect(body["name"]).to eq "Gender"
      expect(body["type"]).to eq "dropdown"
      expect(body["options"]["choices"]).to eq %w[m w]
    end
  end

  context "#create_segment_operation" do
    subject { client.create_segment_operation("a") }

    it "POSTs to segments list resource" do
      expect(subject[:method]).to eq "POST"
      expect(subject[:path]).to eq "lists/2/segments"
    end

    it "body includes name and static_segment fields" do
      body = JSON.parse(subject[:body])
      expect(body["name"]).to eq "a"
      expect(body["static_segment"]).to eq []
    end
  end

  context "#create_segment executes batch" do
    before { expect(Settings.mailchimp).to receive(:max_attempts).and_return(3) }

    it "raises if status does not change within max_attempts" do
      stub_request(:post, "https://us12.api.mailchimp.com/3.0/batches")
        .to_return(status: 200, body: {id: 1}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1")
        .to_return(status: 200, body: {id: 1, status: "pending"}.to_json)

      expect(client).to receive(:sleep).exactly(5).times
      expect do
        client.create_segments(%w[a])
      end.to raise_error RuntimeError, "Batch 1 exeeded max_attempts, status: pending"
    end

    it "succeeds if status changes to finished and fetches batch result tgz" do
      stub_request(:post, "https://us12.api.mailchimp.com/3.0/batches")
        .to_return(status: 200, body: {id: 1}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1")
        .to_return(status: 200, body: {id: 1, status: "pending"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "finished", response_body_url: "https://us12.api.mailchimp.com/3.0/batches/1/result"}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1/result")
        .to_return({status: 400}, {status: 400}, {status: 200,
body: create_tgz([response: {title: :subscriber, detail: "okay", status: 200}.to_json])})
      expect(client).to receive(:sleep).twice
      payload, response = client.create_segments(%w[a])
      expect(response[:operation_results][0][:title]).to eq "subscriber"
      expect(response[:operation_results][0][:detail]).to eq "okay"
      expect(response[:operation_results][0][:status]).to eq 200

      expect(payload[0][:method]).to eq "POST"
      expect(payload[0][:path]).to eq "lists/2/segments"
      expect(payload[0][:body]).to eq ({name: "a", static_segment: []}).to_json
    end

    it "supports multiple response files in tgz" do
      stub_request(:post, "https://us12.api.mailchimp.com/3.0/batches")
        .to_return(status: 200, body: {id: 1}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1")
        .to_return(status: 200, body: {id: 1, status: "finished", response_body_url: "https://us12.api.mailchimp.com/3.0/batches/1/result"}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1/result")
        .to_return({status: 200, body: create_tgz(
          [response: {title: :subscriber}.to_json],
          [response: {title: :subscriber_2}.to_json]
        )})
      expect(client).to receive(:sleep).once
      _payload, response = client.create_segments(%w[a])
      expect(response[:operation_results].pluck(:title)).to match_array(["subscriber", "subscriber_2"])
    end

    it "retries fetching of tgz if it fails" do
      stub_request(:post, "https://us12.api.mailchimp.com/3.0/batches")
        .to_return(status: 200, body: {id: 1}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1")
        .to_return(status: 200, body: {id: 1, status: "pending"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "finished", response_body_url: "https://us12.api.mailchimp.com/3.0/batches/1/result"}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1/result")
        .to_return(status: 200, body: create_tgz([response: {title: :subscriber, detail: "okay", status: 200}.to_json]))
      expect(client).to receive(:sleep).twice
      payload, response = client.create_segments(%w[a])
      expect(response[:operation_results][0][:title]).to eq "subscriber"
      expect(response[:operation_results][0][:detail]).to eq "okay"
      expect(response[:operation_results][0][:status]).to eq 200

      expect(payload[0][:method]).to eq "POST"
      expect(payload[0][:path]).to eq "lists/2/segments"
      expect(payload[0][:body]).to eq ({name: "a", static_segment: []}).to_json
    end

    it "resets counts when status changes" do
      stub_request(:post, "https://us12.api.mailchimp.com/3.0/batches")
        .to_return(status: 200, body: {id: 1}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1")
        .to_return(status: 200, body: {id: 1, status: "pending"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "pending"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "pending"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "pre-processing"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "pre-processing"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "pre-processing"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "started"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "started"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "started"}.to_json)
        .to_return(status: 200, body: {id: 1, status: "finished", response_body_url: "https://us12.api.mailchimp.com/3.0/batches/1/result"}.to_json)

      stub_request(:get, "https://us12.api.mailchimp.com/3.0/batches/1/result")
        .to_return(status: 200, body: create_tgz([]))

      expect(client).to receive(:sleep).exactly(10).times
      client.create_segments(%w[a])
    end
  end

  context "#update_segment_operation" do
    subject {
      client.update_segment_operation(1,
        {members_to_add: %w[leader@example.com member@example.com], members_to_remove: []})
    }

    it "POSTs to segments list resource" do
      expect(subject[:method]).to eq "POST"
      expect(subject[:path]).to eq "lists/2/segments/1"
    end

    it "passes body as json" do
      body = JSON.parse(subject[:body])
      expect(body["members_to_add"]).to eq %w[leader@example.com member@example.com]
      expect(body["members_to_remove"]).to eq []
    end
  end

  context "#subscribe_member_operation" do
    subject { client.subscribe_member_operation(top_leader) }

    it "POSTs to members list resource" do
      expect(subject[:method]).to eq "POST"
      expect(subject[:path]).to eq "lists/2/members"
    end

    it "body includes status, email_address, FNAME and LNAME fields" do
      body = JSON.parse(subject[:body])
      expect(body["status"]).to eq "subscribed"
      expect(body["email_address"]).to eq "top_leader@example.com"
      expect(body["merge_fields"]["FNAME"]).to eq "Top"
      expect(body["merge_fields"]["LNAME"]).to eq "Leader"
    end

    it "body includes member fields" do
      member_field = ["id", ->(p) { p.id }]
      expect(client).to receive(:member_fields).and_return([member_field])
      body = JSON.parse(subject[:body])
      expect(body["id"]).to eq top_leader.id
    end

    it "body includes merge fields" do
      merge_field = ["Gender", "dropdown", {choices: %w[w m]}, ->(p) { p.gender }]
      expect(client).to receive(:merge_fields).and_return([merge_field])
      body = JSON.parse(subject[:body])
      expect(body["merge_fields"]["GENDER"]).to eq top_leader.gender
    end
  end

  context "#update_member_operation" do
    subject { client.update_member_operation(top_leader) }

    it "POSTs to members list resource" do
      expect(subject[:method]).to eq "PUT"
      expect(subject[:path]).to eq "lists/2/members/f55f27b511af2735650c330490da54f5"
    end

    it "body includes status, email_address, FNAME and LNAME fields" do
      body = JSON.parse(subject[:body])
      expect(body["email_address"]).to eq "top_leader@example.com"
      expect(body["merge_fields"]["FNAME"]).to eq "Top"
      expect(body["merge_fields"]["LNAME"]).to eq "Leader"
    end

    it "body includes member fields" do
      member_field = ["id", ->(p) { p.id }]
      expect(client).to receive(:member_fields).and_return([member_field])
      body = JSON.parse(subject[:body])
      expect(body["id"]).to eq top_leader.id
    end

    it "body includes merge fields" do
      merge_field = ["Gender", "dropdown", {choices: %w[w m]}, ->(p) { p.gender }]
      expect(client).to receive(:merge_fields).and_return([merge_field])
      body = JSON.parse(subject[:body])
      expect(body["merge_fields"]["GENDER"]).to eq top_leader.gender
    end
  end

  context "#unsubscribe_member_operation" do
    subject { client.unsubscribe_member_operation(@email) }

    it "DELETEs email specific resource" do
      @email = "top_leader@example.com"
      expect(subject[:method]).to eq "DELETE"
      expect(subject[:path]).to eq "lists/2/members/f55f27b511af2735650c330490da54f5"
    end

    it "ignores case when calculating id" do
      @email = "TOP_LEADER@EXAMPLE.COM"
      expect(subject[:path]).to eq "lists/2/members/f55f27b511af2735650c330490da54f5"
    end

    it "has different has for differnt email" do
      @email = "top_leader1@example.com"
      expect(subject[:path]).to eq "lists/2/members/d36e5c76dc67d95e935265cc451fc878"
    end
  end
end
