require 'cgi'
require 'rails_helper'
require 'securerandom'


RSpec.describe 'Wiki API', type: :request do
  let!(:user) { create_member_user! }

  let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
  let!(:page) do
    WikiPage.create!(tag_name: tn, created_user: user, updated_user: user).tap do |p|
      Wiki::Commit.content!(page: p, body: 'init', created_user: user, message: 'init')
    end
  end

  describe 'GET /wiki' do
    it 'returns wiki pages with title' do
      get '/wiki'

      expect(response).to have_http_status(:ok)

      expect(json).to be_an(Array)
      expect(json).not_to be_empty

      expect(json[0]).to have_key('title')
      expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
    end
  end

  describe 'GET /wiki/:id' do
    subject(:request) do
      get "/wiki/#{ page_id }"
    end

    let(:page_id) { page.id }

    context 'when wiki page exists' do
      it 'returns wiki page with title' do
        request
        expect(response).to have_http_status(:ok)

        expect(json).to include(
            'id'        => page.id,
            'title'     => 'spec_wiki_title')
      end
    end

    context 'when wiki page does not exist' do
      let(:page_id) { 9_999_999 }

      it 'returns 404' do
        request
        expect(response).to have_http_status(:not_found)
      end
    end
  end

  describe 'POST /wiki' do
    let(:endpoint) { '/wiki' }

    let(:member) { create(:user, role: 'member') }
    let(:guest) { create(:user, role: 'guest') }

    def auth_headers(user)
      { 'X-Transfer-Code' => user.inheritance_code }
    end

    context 'when not logged in' do
      it 'returns 401' do
        post endpoint, params: { title: 'Test', body: 'Hello' }
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'when logged in but not member' do
      it 'returns 403' do
        post endpoint, params: { title: 'Test', body: 'Hello' }, headers: auth_headers(guest)
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'when params invalid' do
      it 'returns 422 when title blank' do
        post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
        expect(response).to have_http_status(:unprocessable_entity)
      end

      it 'returns 422 when body blank' do
        post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end

    context 'when success' do
      it 'creates wiki_page and first content revision' do
        expect do
          post endpoint, params: { title: 'TestPage', body: "a\nb\nc", message: 'init' },
                         headers: auth_headers(member)
        end
            .to change(WikiPage, :count).by(1)
            .and change(WikiRevision, :count).by(1)

        expect(response).to have_http_status(:created)

        page_id = json.fetch('id')
        expect(json.fetch('title')).to eq('TestPage')

        page = WikiPage.find(page_id)
        rev = page.current_revision
        expect(rev).to be_present
        expect(rev).to be_content
        expect(rev.message).to eq('init')

        # body が復元できること
        expect(page.body).to eq("a\nb\nc")

        # 行数とリレーションの整合
        expect(rev.lines_count).to eq(3)
        expect(rev.wiki_revision_lines.order(:position).pluck(:position)).to eq([0, 1, 2])
        expect(rev.wiki_lines.pluck(:body)).to match_array(%w[a b c])
      end

      it 'reuses existing WikiLine rows by sha256' do
        # 先に同じ行を作っておく
        WikiLine.create!(sha256: Digest::SHA256.hexdigest('a'), body: 'a', created_at: Time.current, updated_at: Time.current)

        post endpoint,
             params: { title: 'Reuse', body: "a\na" },
             headers: auth_headers(member)

        page = WikiPage.find(JSON.parse(response.body).fetch('id'))
        rev = page.current_revision
        expect(rev.lines_count).to eq(2)

        # "a" の WikiLine が増殖しない（1行のはず）
        expect(WikiLine.where(body: 'a').count).to eq(1)
      end
    end
  end

  describe 'PUT /wiki/:id' do
    let(:member) { create(:user, role: 'member', inheritance_code: SecureRandom.hex(16)) }
    let(:guest)  { create(:user, role: 'guest',  inheritance_code: SecureRandom.hex(16)) }

    def auth_headers(user)
      { 'X-Transfer-Code' => user.inheritance_code }
    end

    #let!(:page) { create(:wiki_page, title: 'TestPage') }
    let!(:page) do
      build(:wiki_page, title: 'TestPage').tap do |p|
        puts p.errors.full_messages unless p.valid?
        p.save!
      end
    end

    before do
      # 初期版を 1 つ作っておく（更新が“2版目”になるように）
      Wiki::Commit.content!(page: page, body: "a\nb", created_user: member, message: 'init')
    end

    context 'when not logged in' do
      it 'returns 401' do
        put "/wiki/#{page.id}", params: { title: 'TestPage', body: 'x' }
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'when logged in but not member' do
      it 'returns 403' do
        put "/wiki/#{page.id}",
            params: { title: 'TestPage', body: 'x' },
            headers: auth_headers(guest)
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'when params invalid' do
      it 'returns 422 when body blank' do
        put "/wiki/#{page.id}",
            params: { title: 'TestPage', body: '' },
            headers: auth_headers(member)
        expect(response).to have_http_status(:unprocessable_entity)
      end

      it 'returns 422 when title mismatched (if you forbid rename here)' do
        put "/wiki/#{page.id}",
            params: { title: 'OtherTitle', body: 'x' },
            headers: auth_headers(member)
        # 君の controller 例だと title 変更は 422 にしてた
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end

    context 'when success' do
      it 'creates new revision and returns 200' do
        current_id = page.wiki_revisions.maximum(:id)

        expect do
          put "/wiki/#{page.id}",
              params: { title: 'TestPage', body: "x\ny", message: 'edit', base_revision_id: current_id },
              headers: auth_headers(member)
        end.to change(WikiRevision, :count).by(1)

        expect(response).to have_http_status(:ok)

        page.reload
        rev = page.current_revision
        expect(rev).to be_content
        expect(rev.message).to eq('edit')
        expect(page.body).to eq("x\ny")
        expect(rev.base_revision_id).to eq(current_id)
      end
    end

    # TODO: コンフリクト未実装のため，実装したらコメント外す．
    # context 'when conflict' do
    #   it 'returns 409 when base_revision_id mismatches' do
    #     # 先に別ユーザ（同じ member でもOK）が 1 回更新して先頭を進める
    #     Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit')
    #     page.reload

    #     stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id
    #     put "/wiki/#{page.id}",
    #         params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
    #         headers: auth_headers(member)

    #     expect(response).to have_http_status(:conflict)
    #     json = JSON.parse(response.body)
    #     expect(json['error']).to eq('conflict')
    #   end
    # end

    context 'when page not found' do
      it 'returns 404' do
        put "/wiki/99999999",
            params: { title: 'X', body: 'x' },
            headers: auth_headers(member)
        expect(response).to have_http_status(:not_found)
      end
    end
  end

  describe 'GET /wiki/title/:title' do
    it 'returns wiki page by title' do
      get "/wiki/title/#{CGI.escape('spec_wiki_title')}"

      expect(response).to have_http_status(:ok)

      expect(json).to have_key('id')
      expect(json).to have_key('title')
      expect(json['id']).to eq(page.id)
      expect(json['title']).to eq('spec_wiki_title')
    end

    it 'returns 404 when not found' do
      get "/wiki/title/#{ CGI.escape('nope') }"
      expect(response).to have_http_status(:not_found)
    end
  end

  describe 'GET /wiki/search' do
    before do
      # 追加で検索ヒット用
      TagName.create!(name: 'spec_wiki_title_2')
      WikiPage.create!(tag_name: TagName.find_by!(name: 'spec_wiki_title_2'),
                       created_user: user, updated_user: user)

      TagName.create!(name: 'unrelated_title')
      WikiPage.create!(tag_name: TagName.find_by!(name: 'unrelated_title'),
                       created_user: user, updated_user: user)
    end

    it 'returns up to 20 pages filtered by title like' do
      get "/wiki/search?title=#{CGI.escape('spec_wiki')}"

      expect(response).to have_http_status(:ok)
      expect(json).to be_an(Array)

      titles = json.map { |p| p['title'] }
      expect(titles).to include('spec_wiki_title', 'spec_wiki_title_2')
      expect(titles).not_to include('unrelated_title')
    end

    it 'returns all when title param is blank' do
      get "/wiki/search?title=#{CGI.escape('')}"

      expect(response).to have_http_status(:ok)
      expect(json).to be_an(Array)
      expect(json.map { |p| p['title'] }).to include('spec_wiki_title')
    end
  end

  describe 'GET /wiki/changes' do
    let!(:rev1) do
      Wiki::Commit.content!(page: page, body: "a\nb", created_user: user, message: 'r1')
      page.current_revision
    end

    let!(:rev2) do
      Wiki::Commit.content!(page: page, body: "a\nc", created_user: user, message: 'r2')
      page.current_revision
    end

    it 'returns latest revisions (optionally filtered by page id)' do
      get "/wiki/changes?id=#{page.id}"

      expect(response).to have_http_status(:ok)
      expect(json).to be_an(Array)
      expect(json).not_to be_empty

      top = json.first
      expect(top).to include(
        'revision_id' => rev2.id,
        'pred' => rev2.base_revision_id,
        'succ' => nil,
        'kind' => 'content',
        'message' => 'r2'
      )
      expect(top['wiki_page']).to include('id' => page.id, 'title' => 'spec_wiki_title')
      expect(top['user']).to include('id' => user.id, 'name' => user.name)
      expect(top).to have_key('timestamp')

      # order desc をざっくり担保
      ids = json.map { |x| x['revision_id'] }
      expect(ids).to eq(ids.sort.reverse)
    end

    it 'returns empty array when page has no revisions and filtered by id' do
      # 別ページを作って revision 無し
      tn2 = TagName.create!(name: 'spec_no_rev')
      p2 = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)

      get "/wiki/changes?id=#{p2.id}"
      expect(response).to have_http_status(:ok)
      expect(json).to eq([])
    end
  end

  describe 'GET /wiki/title/:title/exists' do
    it 'returns 204 when exists' do
      get "/wiki/title/#{CGI.escape('spec_wiki_title')}/exists"
      expect(response).to have_http_status(:no_content)
      expect(response.body).to be_empty
    end

    it 'returns 404 when not exists' do
      get "/wiki/title/#{CGI.escape('nope')}/exists"
      expect(response).to have_http_status(:not_found)
    end
  end

  describe 'GET /wiki/:id/exists' do
    it 'returns 204 when exists' do
      get "/wiki/#{page.id}/exists"
      expect(response).to have_http_status(:no_content)
      expect(response.body).to be_empty
    end

    it 'returns 404 when not exists' do
      get "/wiki/99999999/exists"
      expect(response).to have_http_status(:not_found)
    end
  end

  describe 'GET /wiki/:id/diff' do
    let!(:rev_a) do
      Wiki::Commit.content!(page: page, body: "a\nb\nc", created_user: user, message: 'A')
      page.current_revision
    end

    let!(:rev_b) do
      Wiki::Commit.content!(page: page, body: "a\nx\nc", created_user: user, message: 'B')
      page.current_revision
    end

    it 'returns diff json between revisions' do
      get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{rev_b.id}"

      expect(response).to have_http_status(:ok)

      expect(json).to include(
        'wiki_page_id' => page.id,
        'title' => 'spec_wiki_title',
        'older_revision_id' => rev_a.id,
        'newer_revision_id' => rev_b.id
      )
      expect(json['diff']).to be_an(Array)
      # ざっくり「b が消えて x が増えた」が含まれることを確認
      types = json['diff'].map { |x| x['type'] }
      expect(types).to include('removed', 'added').or include('removed').and include('added')
    end

    it 'uses latest as "to" when to is omitted' do
      get "/wiki/#{page.id}/diff?from=#{rev_a.id}"

      expect(response).to have_http_status(:ok)
      expect(json['older_revision_id']).to eq(rev_a.id)
      expect(json['newer_revision_id']).to eq(page.current_revision.id)
    end

    it 'returns 422 when "to" is redirect revision' do
      # redirect revision を作る
      tn2 = TagName.create!(name: 'redirect_target')
      target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)

      Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir')
      redirect_rev = page.current_revision
      expect(redirect_rev).to be_redirect

      get "/wiki/#{page.id}/diff?from=#{rev_a.id}&to=#{redirect_rev.id}"
      expect(response).to have_http_status(:unprocessable_entity)
    end

    it 'returns 422 when "from" is redirect revision' do
      tn2 = TagName.create!(name: 'redirect_target2')
      target = WikiPage.create!(tag_name: tn2, created_user: user, updated_user: user)

      Wiki::Commit.redirect!(page: page, redirect_page: target, created_user: user, message: 'redir2')
      redirect_rev = page.current_revision

      get "/wiki/#{page.id}/diff?from=#{redirect_rev.id}&to=#{rev_b.id}"
      expect(response).to have_http_status(:unprocessable_entity)
    end
  end
end
