require 'rails_helper'

describe Linear do
  let(:access_token) { 'valid_access_token' }
  let(:url) { 'https://api.linear.app/graphql' }
  let(:linear_client) { described_class.new(access_token) }
  let(:headers) { { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{access_token}" } }

  it 'raises an exception if the API key is absent' do
    expect { described_class.new(nil) }.to raise_error(ArgumentError, 'Missing Credentials')
  end

  context 'when querying teams' do
    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200,
                     body: {  success: true, data: { teams: { nodes: [{ id: 'team1', name: 'Team One' }] } } }.to_json,
                     headers: headers)
      end

      it 'returns team data' do
        response = linear_client.teams
        expect(response).to eq({ 'teams' => { 'nodes' => [{ 'id' => 'team1', 'name' => 'Team One' }] } })
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
                     headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.teams
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
      end
    end
  end

  context 'when querying team entities' do
    let(:team_id) { 'team1' }

    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200,
                     body: { success: true, data: {
                       users: { nodes: [{ id: 'user1', name: 'User One' }] },
                       projects: { nodes: [{ id: 'project1', name: 'Project One' }] },
                       workflowStates: { nodes: [] },
                       issueLabels: { nodes: [{ id: 'bug', name: 'Bug' }] }
                     } }.to_json,
                     headers: headers)
      end

      it 'returns team entities' do
        response = linear_client.team_entities(team_id)
        expect(response).to eq({
                                 'users' => { 'nodes' => [{ 'id' => 'user1', 'name' => 'User One' }] },
                                 'projects' => { 'nodes' => [{ 'id' => 'project1', 'name' => 'Project One' }] },
                                 'workflowStates' => { 'nodes' => [] },
                                 'issueLabels' => { 'nodes' => [{ 'id' => 'bug', 'name' => 'Bug' }] }
                               })
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
                     headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.team_entities(team_id)
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
      end
    end
  end

  context 'when creating an issue' do
    let(:params) do
      {
        title: 'Title',
        team_id: 'team1',
        description: 'Description',
        assignee_id: 'user1',
        priority: 1,
        label_ids: ['bug']
      }
    end
    let(:user) { instance_double(User, name: 'John Doe', avatar_url: 'https://example.com/avatar.jpg') }

    context 'when description contains double quotes' do
      it 'produces valid GraphQL by escaping the quotes' do
        allow(linear_client).to receive(:post) do |payload|
          expect(payload[:query]).to include('description: "the sender is \\"Bot\\"')
          instance_double(HTTParty::Response, success?: true,
                                              parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } })
        end

        linear_client.create_issue(params.merge(description: 'the sender is "Bot"'))
      end
    end

    context 'when description contains backslashes' do
      it 'produces valid GraphQL by escaping the backslashes' do
        allow(linear_client).to receive(:post) do |payload|
          expect(payload[:query]).to include('description: "path\\\\to\\\\file"')
          instance_double(HTTParty::Response, success?: true,
                                              parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } })
        end

        linear_client.create_issue(params.merge(description: 'path\\to\\file'))
      end
    end

    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200, body: { success: true, data: { issueCreate: { id: 'issue1', title: 'Title' } } }.to_json, headers: headers)
      end

      it 'creates an issue' do
        response = linear_client.create_issue(params)
        expect(response).to eq({ 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } })
      end

      context 'when user is provided' do
        it 'includes user attribution in the request' do
          allow(linear_client).to receive(:post) do |payload|
            expect(payload[:query]).to include('createAsUser: "John Doe"')
            expect(payload[:query]).to include('displayIconUrl: "https://example.com/avatar.jpg"')
            instance_double(HTTParty::Response, success?: true,
                                                parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } })
          end

          linear_client.create_issue(params, user)
        end
      end

      context 'when user has no avatar' do
        let(:user_no_avatar) { instance_double(User, name: 'Jane Doe', avatar_url: '') }

        it 'includes only user name in the request' do
          allow(linear_client).to receive(:post) do |payload|
            expect(payload[:query]).to include('createAsUser: "Jane Doe"')
            expect(payload[:query]).not_to include('displayIconUrl')
            instance_double(HTTParty::Response, success?: true,
                                                parsed_response: { 'data' => { 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title' } } })
          end

          linear_client.create_issue(params, user_no_avatar)
        end
      end

      context 'when the priority is invalid' do
        let(:params) { { title: 'Title', team_id: 'team1', priority: 5 } }

        it 'raises an exception' do
          expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'Invalid priority value. Priority must be 0, 1, 2, 3, or 4.')
        end
      end

      context 'when the label_ids are invalid' do
        let(:params) { { title: 'Title', team_id: 'team1', label_ids: 'bug' } }

        it 'raises an exception' do
          expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'label_ids must be an array of strings.')
        end
      end

      context 'when the title is missing' do
        let(:params) { { team_id: 'team1' } }

        it 'raises an exception' do
          expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'Missing title')
        end
      end

      context 'when the team_id is missing' do
        let(:params) { { title: 'Title' } }

        it 'raises an exception' do
          expect { linear_client.create_issue(params) }.to raise_error(ArgumentError, 'Missing team id')
        end
      end

      context 'when the API key is invalid' do
        before do
          stub_request(:post, url)
            .to_return(status: 401, body: { errors: [{ message: 'Invalid API key' }] }.to_json, headers: headers)
        end

        it 'raises an exception' do
          response = linear_client.create_issue(params)
          expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Invalid API key' }] }, :error_code => 401 })
        end
      end
    end

    context 'when the description is markdown' do
      let(:description) { 'Cmd/Ctrl` `K` **is our most powerful feature.** \n\nUse it to search for or take any action in the app' }

      before do
        stub_request(:post, url)
          .to_return(status: 200, body: { success: true,
                                          data: { issueCreate: { id: 'issue1', title: 'Title',
                                                                 description: description } } }.to_json, headers: headers)
      end

      it 'creates an issue' do
        response = linear_client.create_issue(params)
        expect(response).to eq({ 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title',
                                                    'description' => description } })
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error creating issue' }] }.to_json, headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.create_issue(params)
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error creating issue' }] }, :error_code => 422 })
      end
    end
  end

  context 'when linking an issue' do
    let(:link) { 'https://example.com' }
    let(:issue_id) { 'issue1' }
    let(:title) { 'Title' }
    let(:user) { instance_double(User, name: 'John Doe', avatar_url: 'https://example.com/avatar.jpg') }

    context 'when title contains double quotes' do
      it 'produces valid GraphQL by escaping the quotes' do
        allow(linear_client).to receive(:post) do |payload|
          expect(payload[:query]).to include('title: "say \\"hello\\"')
          instance_double(HTTParty::Response, success?: true,
                                              parsed_response: { 'data' => { 'attachmentLinkURL' => { 'id' => 'attachment1' } } })
        end

        linear_client.link_issue(link, issue_id, 'say "hello"')
      end
    end

    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200, body: { success: true, data: { attachmentLinkURL: { id: 'attachment1' } } }.to_json, headers: headers)
      end

      it 'links an issue' do
        response = linear_client.link_issue(link, issue_id, title)
        expect(response).to eq({ 'attachmentLinkURL' => { 'id' => 'attachment1' } })
      end

      context 'when user is provided' do
        it 'includes user attribution in the request' do
          expected_params = {
            issue_id: issue_id,
            link: link,
            title: title,
            user_name: 'John Doe',
            user_avatar_url: 'https://example.com/avatar.jpg'
          }

          expect(Linear::Mutations).to receive(:issue_link).with(expected_params).and_call_original
          allow(linear_client).to receive(:post).and_return(
            instance_double(HTTParty::Response, success?: true, parsed_response: { 'data' => { 'attachmentLinkURL' => { 'id' => 'attachment1' } } })
          )

          linear_client.link_issue(link, issue_id, title, user)
        end
      end

      context 'when user has no avatar' do
        let(:user_no_avatar) { instance_double(User, name: 'Jane Doe', avatar_url: '') }

        it 'includes only user name in the request' do
          expected_params = {
            issue_id: issue_id,
            link: link,
            title: title,
            user_name: 'Jane Doe'
          }

          expect(Linear::Mutations).to receive(:issue_link).with(expected_params).and_call_original
          allow(linear_client).to receive(:post).and_return(
            instance_double(HTTParty::Response, success?: true, parsed_response: { 'data' => { 'attachmentLinkURL' => { 'id' => 'attachment1' } } })
          )

          linear_client.link_issue(link, issue_id, title, user_no_avatar)
        end
      end

      context 'when the link is missing' do
        let(:link) { '' }

        it 'raises an exception' do
          expect { linear_client.link_issue(link, issue_id, title) }.to raise_error(ArgumentError, 'Missing link')
        end
      end

      context 'when the issue_id is missing' do
        let(:issue_id) { '' }

        it 'raises an exception' do
          expect { linear_client.link_issue(link, issue_id, title) }.to raise_error(ArgumentError, 'Missing issue id')
        end
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error linking issue' }] }.to_json, headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.link_issue(link, issue_id, title)
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error linking issue' }] }, :error_code => 422 })
      end
    end
  end

  context 'when unlinking an issue' do
    let(:link_id) { 'attachment1' }

    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200, body: { success: true, data: { attachmentLinkURL: { id: 'attachment1' } } }.to_json, headers: headers)
      end

      it 'unlinks an issue' do
        response = linear_client.unlink_issue(link_id)
        expect(response).to eq({ 'attachmentLinkURL' => { 'id' => 'attachment1' } })
      end

      context 'when the link_id is missing' do
        let(:link_id) { '' }

        it 'raises an exception' do
          expect { linear_client.unlink_issue(link_id) }.to raise_error(ArgumentError, 'Missing  link id')
        end
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error unlinking issue' }] }.to_json, headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.unlink_issue(link_id)
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error unlinking issue' }] }, :error_code => 422 })
      end
    end
  end

  context 'when querying issues' do
    let(:term) { 'term' }

    context 'when search term contains double quotes' do
      it 'produces valid GraphQL by escaping the quotes' do
        allow(linear_client).to receive(:post) do |payload|
          expect(payload[:query]).to include('term: "find \\"Bot\\"')
          instance_double(HTTParty::Response, success?: true,
                                              parsed_response: { 'data' => { 'searchIssues' => { 'nodes' => [] } } })
        end

        linear_client.search_issue('find "Bot"')
      end
    end

    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200, body: { success: true,
                                          data: { searchIssues: { nodes: [{ id: 'issue1', title: 'Title' }] } } }.to_json, headers: headers)
      end

      it 'returns issues' do
        response = linear_client.search_issue(term)
        expect(response).to eq({ 'searchIssues' => { 'nodes' => [{ 'id' => 'issue1', 'title' => 'Title' }] } })
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
                     headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.search_issue(term)
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
      end
    end
  end

  context 'when querying linked issues' do
    context 'when the API response is success' do
      before do
        stub_request(:post, url)
          .to_return(status: 200, body: { success: true, data: { linkedIssue: { id: 'issue1', title: 'Title' } } }.to_json, headers: headers)
      end

      it 'returns linked issues' do
        response = linear_client.linked_issues('app.chatwoot.com')
        expect(response).to eq({ 'linkedIssue' => { 'id' => 'issue1', 'title' => 'Title' } })
      end
    end

    context 'when the API response is an error' do
      before do
        stub_request(:post, url)
          .to_return(status: 422, body: { errors: [{ message: 'Error retrieving data' }] }.to_json,
                     headers: headers)
      end

      it 'raises an exception' do
        response = linear_client.linked_issues('app.chatwoot.com')
        expect(response).to eq({ :error => { 'errors' => [{ 'message' => 'Error retrieving data' }] }, :error_code => 422 })
      end
    end
  end
end
