# typed: false
require "rails_helper"

describe Domain::Fa::Job::FavsJob do
  let(:http_client_mock) { instance_double("::Scraper::HttpClient") }
  before { Scraper::ClientFactory.http_client_mock = http_client_mock }

  let(:client_mock_config) { [] }
  let!(:log_entries) do
    HttpClientMockHelpers.init_http_client_mock(
      http_client_mock,
      client_mock_config,
    )
  end
  let(:user_url_name) { "zzreg" }
  let(:args) { { url_name: user_url_name } }

  shared_context "user exists" do
    let!(:user) do
      create(:domain_user_fa_user, name: user_url_name, url_name: user_url_name)
    end
  end

  shared_context "user has no favs" do
    let(:client_mock_config) do
      [
        {
          uri: "https://www.furaffinity.net/favorites/zzreg/",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_zzreg_no_favs.html",
            ),
        },
      ]
    end
  end

  shared_context "user is not found" do
    let(:user_url_name) { "caffeinatedarson" }
    let(:client_mock_config) do
      [
        {
          uri: "https://www.furaffinity.net/favorites/caffeinatedarson/",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_account_not_found_user_caffeinatedarson.html",
            ),
        },
      ]
    end
  end

  shared_context "user is disabled" do
    let(:user_url_name) { "ground-lion" }
    let(:client_mock_config) do
      [
        {
          uri: "https://www.furaffinity.net/favorites/ground-lion/",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_account_disabled_user_ground_lion.html",
            ),
        },
      ]
    end
  end

  context "the user does not yet exist" do
    include_context "user has no favs"

    it "creates the user" do
      perform_now(args)
      expect(Domain::User::FaUser.find_by(url_name: "zzreg")).not_to be_nil
    end

    it "enqueues a page scan job" do
      perform_now(args)
      user = Domain::User::FaUser.find_by(url_name: "zzreg")
      expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::UserPageJob)).to match(
        [including({ user:, caused_by_entry: log_entries[0] })],
      )
    end

    it "does not create any new posts" do
      expect do perform_now(args) end.not_to change(
        Domain::Post::FaPost,
        :count,
      )
    end
  end

  context "site indicates no favs" do
    include_context "user exists"
    include_context "user has no favs"

    it "records no favs for the user" do
      perform_now(args)
      expect(user.faved_posts).to eq([])
    end
  end

  context "user has had a recent favs scan" do
    include_context "user exists"
    it "does not re-scan the user" do
      user.scanned_favs_at = old_scanned_at = 1.day.ago
      user.save!
      perform_now(args)
      expect(user.scanned_favs_at).to be_within(1.second).of(old_scanned_at)
    end
  end

  context "the user is not found" do
    include_context "user exists"
    include_context "user is not found"

    it "marks the user as scanned" do
      expect do
        perform_now(args)
        user.reload
      end.to change(user, :scanned_favs_at).from(nil).to(
        be_within(3.seconds).of(Time.now),
      )
    end
    it "changes user account state to error" do
      expect do
        perform_now(args)
        user.reload
      end.to change(user, :state).from("ok").to("error")
    end
  end

  context "the user is disabled" do
    include_context "user exists"
    include_context "user is disabled"

    it "marks the user as scanned" do
      expect do
        perform_now(args)
        user.reload
      end.to change(user, :scanned_favs_at).from(nil).to(
        be_within(3.seconds).of(Time.now),
      )
    end
    it "changes user account state to account_disabled" do
      expect do
        perform_now(args)
        user.reload
      end.to change(user, :state).from("ok").to("account_disabled")
    end
  end

  context "site indicates favs" do
    include_context "user exists"
    let(:fa_ids) do
      [52_106_426, 36_755_337, 40_769_488, 20_808_448, 20_585_829]
    end

    let(:client_mock_config) do
      [
        {
          uri: "https://www.furaffinity.net/favorites/zzreg/",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_zzreg_page_0_first.html",
            ),
        },
        {
          uri: "https://www.furaffinity.net/favorites/zzreg/1074627373/next",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_zzreg_page_1_1074627373.html",
            ),
          caused_by_entry_idx: 0,
        },
        {
          uri: "https://www.furaffinity.net/favorites/zzreg/475297391/next",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_zzreg_page_2_475297391.html",
            ),
          caused_by_entry_idx: 0,
        },
      ]
    end

    it "records favs for the user" do
      expect do perform_now(args) end.to change(
        Domain::Post::FaPost,
        :count,
      ).by(5)

      posts = Domain::Post::FaPost.where(fa_id: fa_ids)
      expect(user.faved_posts).to match_array(posts)
    end

    it "creates a favs scan job event" do
      expect do perform_now(args) end.to change(
        Domain::UserJobEvent::AddTrackedObject,
        :count,
      ).by(1)

      expect(user.favs_scans.count).to eq(1)
      scan = user.favs_scans.first
      expect(scan.user).to eq(user)
      expect(scan.num_added).to eq(5)
      expect(scan.num_total).to eq(5)
      expect(scan.duration_since_last_scan).to be_nil
      expect(scan.log_entry).to eq(log_entries[0])
      expect(scan.kind).to eq("favs")
    end

    it "creates missing users" do
      expect(Domain::User::FaUser.find_by(url_name: "sepulte")).to be_nil
      expect do perform_now(args) end.to change(
        Domain::User::FaUser,
        :count,
      ).by(5)
      post = Domain::Post::FaPost.find_by(fa_id: 52_106_426)
      expect(post).not_to be_nil
      expect(post.creator.url_name).to eq("sepulte")
    end

    it "creates missing posts" do
      expect(Domain::Post::FaPost.find_by(fa_id: 52_106_426)).to be_nil
      expect do perform_now(args) end.to change(
        Domain::Post::FaPost,
        :count,
      ).by(5)
      post = Domain::Post::FaPost.find_by(fa_id: 52_106_426)
      expect(post).not_to be_nil
    end

    it "updates scanned_favs_at" do
      perform_now(args)
      user.reload
      expect(user.scanned_favs_at).to be_within(3.seconds).of(Time.now)
    end

    it "marks removed posts as ok when seen" do
      post = create(:domain_post_fa_post, fa_id: 52_106_426, state: "removed")
      expect do
        perform_now(args)
        post.reload
      end.to change(post, :state).from("removed").to("ok")
      expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob)).to match(
        array_including(hash_including(post:, caused_by_entry: log_entries[0])),
      )
    end

    it "enqueues post scans" do
      expect do perform_now(args) end.to change(
        Domain::Post::FaPost,
        :count,
      ).by(5)
      expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob)).to match(
        array_including(
          hash_including(
            post: Domain::Post::FaPost.find_by(fa_id: 52_106_426),
            caused_by_entry: log_entries[0],
          ),
          hash_including(
            post: Domain::Post::FaPost.find_by(fa_id: 36_755_337),
            caused_by_entry: log_entries[0],
          ),
          hash_including(
            post: Domain::Post::FaPost.find_by(fa_id: 40_769_488),
            caused_by_entry: log_entries[0],
          ),
          hash_including(
            post: Domain::Post::FaPost.find_by(fa_id: 20_808_448),
            caused_by_entry: log_entries[0],
          ),
          hash_including(
            post: Domain::Post::FaPost.find_by(fa_id: 20_585_829),
            caused_by_entry: log_entries[0],
          ),
        ),
      )
    end

    context "the user model already has favs recorded" do
      it "adds favs newly present" do
        perform_now(args)
        posts = Domain::Post::FaPost.where(fa_id: fa_ids)
        expect(user.faved_posts).to match_array(posts)
      end

      it "creates new FA post models and enqueues scans" do
        p1 = Domain::Post::FaPost.create!(fa_id: fa_ids[0], creator: user)
        expect do perform_now(args) end.to change(
          Domain::Post::FaPost,
          :count,
        ).by(4)

        user.reload
        expect(user.faved_posts).to match_array(
          Domain::Post::FaPost.where(fa_id: fa_ids),
        )

        p1.reload
        expect(p1.faving_users).to eq([user])
      end
    end

    context "the user has previously been scanned" do
      include_context "user exists"

      before do
        perform_now(args)
        user.update_attribute(:scanned_favs_at, 100.days.ago)
      end

      it "stops scanning after the first page" do
        HttpClientMockHelpers.init_http_client_mock(
          http_client_mock,
          client_mock_config[0...1],
        )
        perform_now(args)
        user.reload
        expect(user.faved_posts.count).to eq(5)
        expect(user.scanned_favs_at).to be_within(3.seconds).of(Time.now)
      end

      it "scans all pages if full_scan is true" do
        HttpClientMockHelpers.init_http_client_mock(
          http_client_mock,
          client_mock_config,
        )
        perform_now(args.merge(full_scan: true))
        user.reload
        expect(user.faved_posts.count).to eq(5)
        expect(user.scanned_favs_at).to be_within(3.seconds).of(Time.now)
      end
    end

    context "incremental scan behavior" do
      include_context "user exists"
      let(:fa_ids_page1) { [52_106_426, 36_755_337] }
      let(:fa_ids_page2) { [40_769_488, 20_808_448] }
      let(:fa_ids_page3) { [20_585_829] }

      let(:client_mock_config) do
        [
          {
            uri: "https://www.furaffinity.net/favorites/zzreg/",
            status_code: 200,
            content_type: "text/html",
            contents:
              SpecUtil.read_fixture_file(
                "domain/fa/favorites/favs_zzreg_page_0_first.html",
              ),
          },
          {
            uri: "https://www.furaffinity.net/favorites/zzreg/1074627373/next",
            status_code: 200,
            content_type: "text/html",
            contents:
              SpecUtil.read_fixture_file(
                "domain/fa/favorites/favs_zzreg_page_1_1074627373.html",
              ),
            caused_by_entry_idx: 0,
          },
        ]
      end

      before do
        # Create some existing favs that would be on page 2 and 3
        existing_posts = [
          create(:domain_post_fa_post, fa_id: fa_ids_page2[0]),
          create(:domain_post_fa_post, fa_id: fa_ids_page2[1]),
          create(:domain_post_fa_post, fa_id: fa_ids_page3[0]),
        ]
        user.faved_posts << existing_posts
        user.update!(scanned_favs_at: Time.now - 2.years.to_i)
      end

      it "does not remove existing favs from the user when the post is not on the page" do
        post = create(:domain_post_fa_post)
        user.faved_posts << post
        perform_now(args)
        user.reload
        expect(user.faved_posts).to include(post)
      end

      it "stops scanning when no new favs are found and adds posts from scanned pages" do
        # Should only create posts from page 1 since those are the only new ones
        expect do perform_now(args) end.to change(
          Domain::Post::FaPost,
          :count,
        ).by(2)

        # Should have added the new posts from page 1 to user's favs
        user.reload
        expect(
          user.faved_posts.where(fa_id: fa_ids_page1).pluck(:fa_id),
        ).to match_array(fa_ids_page1)

        # Should still have the existing favs
        expect(
          user
            .faved_posts
            .where(fa_id: fa_ids_page2 + fa_ids_page3)
            .pluck(:fa_id),
        ).to match_array(fa_ids_page2 + fa_ids_page3)

        # Should have updated scanned_favs_at
        expect(user.scanned_favs_at).to be_within(3.seconds).of(Time.now)
      end

      it "creates a favs scan job event" do
        expect do perform_now(args) end.to change(
          Domain::UserJobEvent::AddTrackedObject,
          :count,
        ).by(1)
      end

      it "records the number of favs added" do
        perform_now(args)
        favs_scan = Domain::UserJobEvent::AddTrackedObject.last
        expect(favs_scan.log_entry).to eq(log_entries[0])
        expect(favs_scan.num_added).to eq(2)
        expect(favs_scan.num_total).to eq(5)
        expect(favs_scan.duration_since_last_scan).to be_within(3.seconds).of(
          2.years,
        )
      end
    end
  end

  context "2024 format for favs pages" do
    include_context "user exists"
    let(:client_mock_config) do
      [
        {
          uri: "https://www.furaffinity.net/favorites/zzreg/",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/parser/redux/favs_zzreg_page_1_2024_format.html",
            ),
        },
        {
          uri: "https://www.furaffinity.net/favorites/zzreg/1436907409/next",
          status_code: 200,
          content_type: "text/html",
          contents:
            SpecUtil.read_fixture_file(
              "domain/fa/parser/redux/favs_zzreg_page_last_2024_format.html",
            ),
          caused_by_entry_idx: 0,
        },
      ]
    end

    let(:date_58923196) { Time.parse("May 11, 2023 10:54 AM") }
    let(:date_51810098) { Time.parse("Dec 14, 2024 12:39 AM -08:00") }

    it "records favs for the user" do
      expect do perform_now({ url_name: "zzreg" }) end.to change(
        Domain::Post::FaPost,
        :count,
      ).by(85)

      expect(user.faved_posts.count).to eq(85)
    end

    it "works with old models" do
      post = create(:domain_post_fa_post, fa_id: 58_923_196)
      user.user_post_favs.create!(type: "Domain::UserPostFav", post_id: post.id)
      perform_now({ url_name: "zzreg" })

      sub_58923196 = user.user_post_favs.find { |f| f.post.fa_id == 58_923_196 }
      expect(sub_58923196).to be_a(Domain::UserPostFav)

      sub_51810098 = user.user_post_favs.find { |f| f.post.fa_id == 51_810_098 }
      expect(sub_51810098).to be_a(Domain::UserPostFav::FaUserPostFav)
      expect(sub_51810098.fav_id).to eq(1_724_359_446)
      expect(sub_51810098.explicit_time).to eq(date_51810098)
    end

    it "records favs with fav_id and explicit_time" do
      # assume a record for 58923196 already exists with a date
      # but no fav_fa_id
      post = create(:domain_post_fa_post, fa_id: 58_923_196)
      user.update_fav_model(post_id: post.id, explicit_time: date_58923196)

      perform_now({ url_name: "zzreg" })
      user_post_favs = user.user_post_favs
      expect(user_post_favs.count).to eq(85)

      # the first record should have a date
      sub_51810098 = user.user_post_favs.find { |f| f.post.fa_id == 51_810_098 }
      expect(sub_51810098.fav_id).to eq(1_724_359_446)
      expect(sub_51810098.explicit_time).to eq(date_51810098)

      # non-first new records should not have a date populated
      sub_58724276 = user.user_post_favs.find { |f| f.post.fa_id == 58_724_276 }
      expect(sub_58724276.fav_id).to eq(1_724_341_165)
      expect(sub_58724276.explicit_time).to be_nil

      # existing record should have its fav_fa_id populated, and date should be unchanged
      sub_58923196 = user.user_post_favs.find { |f| f.post.fa_id == 58_923_196 }
      expect(sub_58923196.fav_id).to eq(1_715_343_287)
      expect(sub_58923196.explicit_time).to eq(date_58923196)
    end
  end

  describe "#update_favs_and_dates" do
    context "a favs page with duplicate favs on it" do
      it "works" do
        parser =
          Domain::Fa::Parser::Page.new(
            SpecUtil.read_fixture_file(
              "domain/fa/favorites/favs_narse_2023_duplicate_ids.html",
            ),
          )
        user = create(:domain_user_fa_user, url_name: "narse")

        expect do
          Domain::Fa::Job::FavsJob.update_favs_and_dates(
            user:,
            page_parser: parser,
          )
        end.to change { user.reload.user_post_favs.count }.by(47)

        # first fav should have a date
        fav_319542674 = user.user_post_favs.find { |f| f.fav_id == 319_542_674 }
        expect(fav_319542674.explicit_time).to eq Time.parse(
             "Dec 22, 2014 05:59 PM -05:00",
           )
        expect(fav_319542674.post.fa_id).to eq(15_288_097)

        # the same post is listed twice, it should be deduplicated to the larger fav_fa_id
        fav_313566473 = user.user_post_favs.find { |f| f.fav_id == 313_566_473 }
        expect(fav_313566473.explicit_time).to be_nil
        expect(fav_313566473.post.fa_id).to eq(14_971_928)
      end
    end
  end
end
