# typed: false
require "rails_helper"

RSpec.describe Tasks::CreatePostFileFingerprintsTask do
  let(:log_sink) { StringIO.new }
  let(:task) { described_class.new(log_sink: log_sink) }

  before do
    # Mock external dependencies
    allow(ProgressBar).to receive(:create).and_return(
      instance_double(
        ProgressBar::Base,
        progress: 0,
        total: 100,
        "progress=": nil,
      ),
    )
    allow(Domain::PostFileThumbnailJob).to receive(:new).and_return(
      instance_double(Domain::PostFileThumbnailJob, perform: nil),
    )
    allow(ColorLogger).to receive(:quiet).and_yield
  end

  after do
    # Clean up any GlobalState entries created during tests
    GlobalState.find_by(key: "task-create-post-file-fingerprints")&.destroy
  end

  describe "#run" do
    context "with user mode missing user_param" do
      it "raises an error when user mode is used without user_param" do
        expect {
          task.run(
            mode: Tasks::CreatePostFileFingerprintsTask::Mode::User,
            user_param: nil,
          )
        }.to raise_error("need 'user_param' when mode is Mode::User")
      end
    end

    context "with PostFileDescending mode" do
      let!(:post_files) do
        # Create test post files with descending IDs
        5
          .times
          .map do |i|
            post = create(:domain_post_fa_post, fa_id: 500 + i)
            create(:domain_post_file, post: post, state: "ok")
          end
          .sort_by { |p| -p.id }
      end

      it "saves progress every 100 items" do
        # Create 105 post files to trigger progress saving
        105.times do |i|
          post = create(:domain_post_fa_post, fa_id: 600 + i)
          create(:domain_post_file, post: post, state: "ok")
        end

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
        )

        # Should have saved progress at least once
        saved_progress = GlobalState.get("task-create-post-file-fingerprints")
        expect(saved_progress).to be_present
        expect(saved_progress.to_i).to be > 0
      end

      it "starts from specified start_at ID" do
        # Create post files with specific IDs
        target_post_file = post_files[2] # Third post file
        start_at_id = target_post_file.id.to_s

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
          start_at: start_at_id,
        )

        expect(log_sink.string).to include("resuming from:")
      end

      context "with start_at='last' and existing progress" do
        before do
          # Set up saved progress
          target_post_file = post_files[2]
          GlobalState.set(
            "task-create-post-file-fingerprints",
            target_post_file.id.to_s,
          )
        end

        it "resumes from saved progress" do
          task.run(
            mode:
              Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
            start_at: "last",
          )
          expect(log_sink.string).to include("resuming from:")
        end
      end

      context "with start_at='last' and no existing progress" do
        it "starts from beginning when no saved progress found" do
          task.run(
            mode:
              Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
            start_at: "last",
          )

          expect(log_sink.string).to include("no saved progress")
        end
      end
    end

    context "with PostsDescending mode" do
      let!(:posts) do
        3.times.map { |i| create(:domain_post_fa_post, fa_id: 400 + i) }
      end

      before do
        # Add files to posts so they actually get processed
        posts.each { |post| create(:domain_post_file, post: post) }
      end

      it "processes posts in descending order" do
        # Set up mocks before running the task
        expect(Domain::PostFile::Thumbnail).to receive(
          :create_for_post_file!,
        ).exactly(3).times
        expect(Domain::PostFile::BitFingerprint).to receive(
          :create_for_post_file!,
        ).exactly(3).times

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostsDescending,
        )
      end

      it "uses correct progress bar total" do
        progress_bar =
          instance_double(
            ProgressBar::Base,
            progress: 0,
            total: 100,
            "progress=": nil,
          )
        allow(ProgressBar).to receive(:create).and_return(progress_bar)

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostsDescending,
        )

        expect(ProgressBar).to have_received(:create).with(
          total: 66_431_808,
          progress_mark: " ",
          remainder_mark: " ",
          format: "%B %c/%C (%r/sec) %J%% %a %E",
        )
      end
    end

    context "with User mode" do
      let!(:user) { create(:domain_user_fa_user, url_name: "testuser") }
      let!(:posts) do
        3.times.map do |i|
          create(:domain_post_fa_post, creator: user, fa_id: 300 + i)
        end
      end

      before do
        # Add files to posts so they actually get processed
        posts.each { |post| create(:domain_post_file, post: post) }
      end

      it "processes posts for specified user" do
        # Set up mocks before running the task
        expect(Domain::PostFile::Thumbnail).to receive(
          :create_for_post_file!,
        ).exactly(3).times
        expect(Domain::PostFile::BitFingerprint).to receive(
          :create_for_post_file!,
        ).exactly(3).times

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::User,
          user_param: user.to_param,
        )

        expect(log_sink.string).to include(
          "migrating posts for #{user.to_param}",
        )
      end

      it "raises error when user is not found" do
        expect {
          task.run(
            mode: Tasks::CreatePostFileFingerprintsTask::Mode::User,
            user_param: "nonexistent_user",
          )
        }.to raise_error(
          ActionController::BadRequest,
          "invalid id: \"nonexistent_user\"",
        )
      end

      it "handles posts without creators" do
        # Create a post without a creator
        create(:domain_post_fa_post, creator: nil)

        expect(Domain::PostFile::Thumbnail).to receive(
          :create_for_post_file!,
        ).exactly(3).times
        expect(Domain::PostFile::BitFingerprint).to receive(
          :create_for_post_file!,
        ).exactly(3).times

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::User,
          user_param: user.to_param,
        )
      end
    end

    context "with UsersDescending mode" do
      let!(:users) do
        3.times.map { |i| create(:domain_user_fa_user, url_name: "user#{i}") }
      end

      before do
        # Add posts with files to users
        users.each do |user|
          post = create(:domain_post_fa_post, creator: user)
          create(:domain_post_file, post: post)
        end
      end

      it "processes users in descending order" do
        expect(Domain::PostFile::Thumbnail).to receive(:create_for_post_file!)
          .exactly(3)
          .times
          .and_return([])
        expect(Domain::PostFile::BitFingerprint).to receive(
          :create_for_post_file!,
        ).exactly(3).times.and_return([])

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::UsersDescending,
        )

        users.each do |user|
          expect(log_sink.string).to include(
            "migrating posts for #{user.to_param}",
          )
        end
      end

      it "tracks migrated users in a file" do
        expect(Domain::PostFile::Thumbnail).to receive(
          :create_for_post_file!,
        ).exactly(3).times

        expect(Domain::PostFile::BitFingerprint).to receive(
          :create_for_post_file!,
        ).exactly(3).times

        task.run(
          mode: Tasks::CreatePostFileFingerprintsTask::Mode::UsersDescending,
        )

        # Check that the migrated file was created and contains user params
        if File.exist?("migrated_files.txt")
          migrated_content = File.read("migrated_files.txt")
          users.each do |user|
            expect(migrated_content).to include("#{user.to_param}\n")
          end
        end
      end

      after do
        # Clean up the migrated files
        File.delete("migrated_files.txt") if File.exist?("migrated_files.txt")
      end
    end
  end

  describe "#save_progress" do
    it "saves progress to GlobalState" do
      task.send(:save_progress, 12_345.to_s)

      saved_progress = GlobalState.get("task-create-post-file-fingerprints")
      expect(saved_progress).to eq("12345")
    end
  end

  describe "error handling" do
    let!(:post_file) { create(:domain_post_file, state: "ok") }

    it "handles errors in migrate_post_file gracefully" do
      # Mock the job to raise an error
      allow(Domain::PostFile::Thumbnail).to receive(
        :create_for_post_file!,
      ).and_raise(StandardError.new("Test error"))

      task.run(
        mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
      )

      expect(log_sink.string).to include("error: Test error")
    end

    it "handles errors in migrate_post gracefully" do
      # Create a post with files
      post = create(:domain_post_fa_post, fa_id: 100)
      create(:domain_post_file, post: post)

      allow(Domain::PostFile::Thumbnail).to receive(
        :create_for_post_file!,
      ).and_raise(StandardError.new("Test error"))

      task.run(
        mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostsDescending,
      )

      expect(log_sink.string).to include("error: Test error")
    end
  end

  describe "utility methods" do
    describe "#create_progress_bar" do
      it "creates a progress bar with the correct format" do
        progress_bar = instance_double(ProgressBar::Base)
        allow(ProgressBar).to receive(:create).and_return(progress_bar)

        result = task.send(:create_progress_bar, 1000)

        expect(ProgressBar).to have_received(:create).with(
          total: 1000,
          progress_mark: " ",
          remainder_mark: " ",
          format: "%B %c/%C (%r/sec) %J%% %a %E",
        )
        expect(result).to eq(progress_bar)
      end
    end

    describe "#log" do
      it "writes messages to the log sink" do
        task.send(:log, "Test message")

        expect(log_sink.string).to include("Test message")
      end
    end
  end

  describe "interrupt functionality" do
    let!(:posts) do
      3.times.map { |i| create(:domain_post_fa_post, fa_id: 400 + i) }
    end

    before do
      # Add files to posts so they actually get processed
      posts.each { |post| create(:domain_post_file, post: post) }
    end

    it "inherits interrupt functionality from InterruptableTask" do
      expect(task).to respond_to(:interrupted?)
      expect(task).to be_a(Tasks::InterruptableTask)
    end

    it "stops processing when interrupted in posts_descending mode" do
      # Mock the interrupt to happen after first post
      call_count = 0
      allow(task).to receive(:interrupted?) do
        call_count += 1
        call_count > 1
      end

      expect(Domain::PostFile::Thumbnail).to receive(
        :create_for_post_file!,
      ).at_most(1).times
      expect(Domain::PostFile::BitFingerprint).to receive(
        :create_for_post_file!,
      ).at_most(1).times

      task.run(
        mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostsDescending,
      )
    end

    it "stops processing when interrupted in post_file_descending mode" do
      # Create post files for testing
      3.times do |i|
        post = create(:domain_post_fa_post, fa_id: 500 + i)
        create(:domain_post_file, post: post, state: "ok")
      end

      # Mock the interrupt to happen after first post file
      call_count = 0
      allow(task).to receive(:interrupted?) do
        call_count += 1
        call_count > 1
      end

      expect(Domain::PostFile::Thumbnail).to receive(
        :create_for_post_file!,
      ).at_most(1).times
      expect(Domain::PostFile::BitFingerprint).to receive(
        :create_for_post_file!,
      ).at_most(1).times

      task.run(
        mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
      )
    end

    it "saves progress before interruption in post_file_descending mode" do
      # Create enough post files to trigger progress saving
      105.times do |i|
        post = create(:domain_post_fa_post, fa_id: 600 + i)
        create(:domain_post_file, post: post, state: "ok")
      end

      # Mock the interrupt to happen after progress saving
      call_count = 0
      allow(task).to receive(:interrupted?) do
        call_count += 1
        call_count > 100 # Let it save progress first
      end

      expect(Domain::PostFile::Thumbnail).to receive(
        :create_for_post_file!,
      ).at_least(1).times
      expect(Domain::PostFile::BitFingerprint).to receive(
        :create_for_post_file!,
      ).at_least(1).times

      task.run(
        mode: Tasks::CreatePostFileFingerprintsTask::Mode::PostFileDescending,
      )

      # Should have saved progress before interruption
      saved_progress = GlobalState.get("task-create-post-file-fingerprints")
      expect(saved_progress).to be_present
    end
  end
end
