/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @generated SignedSource<<b0c707529f8b045554b20820c2e3e271>>
 */

mod to_schema;

use to_schema::transform_fixture;
use fixture_tests::test_fixture;

#[test]
fn client_edge_relay_resolver() {
    let input = include_str!("to_schema/fixtures/client-edge-relay-resolver.js");
    let expected = include_str!("to_schema/fixtures/client-edge-relay-resolver.expected");
    test_fixture(transform_fixture, "client-edge-relay-resolver.js", "to_schema/fixtures/client-edge-relay-resolver.expected", input, expected);
}

#[test]
fn client_edge_to_non_null_plural_server_object_relay_resolver_invalid() {
    let input = include_str!("to_schema/fixtures/client-edge-to-non-null-plural-server-object-relay-resolver.invalid.js");
    let expected = include_str!("to_schema/fixtures/client-edge-to-non-null-plural-server-object-relay-resolver.invalid.expected");
    test_fixture(transform_fixture, "client-edge-to-non-null-plural-server-object-relay-resolver.invalid.js", "to_schema/fixtures/client-edge-to-non-null-plural-server-object-relay-resolver.invalid.expected", input, expected);
}

#[test]
fn client_edge_to_plural_server_object_relay_resolver_invalid() {
    let input = include_str!("to_schema/fixtures/client-edge-to-plural-server-object-relay-resolver.invalid.js");
    let expected = include_str!("to_schema/fixtures/client-edge-to-plural-server-object-relay-resolver.invalid.expected");
    test_fixture(transform_fixture, "client-edge-to-plural-server-object-relay-resolver.invalid.js", "to_schema/fixtures/client-edge-to-plural-server-object-relay-resolver.invalid.expected", input, expected);
}

#[test]
fn legacy_relay_resolver_with_root_fragment_on_model() {
    let input = include_str!("to_schema/fixtures/legacy-relay-resolver-with-root-fragment-on-model.js");
    let expected = include_str!("to_schema/fixtures/legacy-relay-resolver-with-root-fragment-on-model.expected");
    test_fixture(transform_fixture, "legacy-relay-resolver-with-root-fragment-on-model.js", "to_schema/fixtures/legacy-relay-resolver-with-root-fragment-on-model.expected", input, expected);
}

#[test]
fn relay_resolver() {
    let input = include_str!("to_schema/fixtures/relay-resolver.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver.expected");
    test_fixture(transform_fixture, "relay-resolver.js", "to_schema/fixtures/relay-resolver.expected", input, expected);
}

#[test]
fn relay_resolver_deprecated() {
    let input = include_str!("to_schema/fixtures/relay-resolver-deprecated.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-deprecated.expected");
    test_fixture(transform_fixture, "relay-resolver-deprecated.js", "to_schema/fixtures/relay-resolver-deprecated.expected", input, expected);
}

#[test]
fn relay_resolver_deprecated_no_description() {
    let input = include_str!("to_schema/fixtures/relay-resolver-deprecated-no-description.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-deprecated-no-description.expected");
    test_fixture(transform_fixture, "relay-resolver-deprecated-no-description.js", "to_schema/fixtures/relay-resolver-deprecated-no-description.expected", input, expected);
}

#[test]
fn relay_resolver_id_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-id.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-id.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-id.invalid.js", "to_schema/fixtures/relay-resolver-id.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_implementing_a_field_defined_by_grandparent_interface() {
    let input = include_str!("to_schema/fixtures/relay-resolver-implementing-a-field-defined-by-grandparent-interface.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-implementing-a-field-defined-by-grandparent-interface.expected");
    test_fixture(transform_fixture, "relay-resolver-implementing-a-field-defined-by-grandparent-interface.js", "to_schema/fixtures/relay-resolver-implementing-a-field-defined-by-grandparent-interface.expected", input, expected);
}

#[test]
fn relay_resolver_implementing_a_field_defined_by_parent_interface() {
    let input = include_str!("to_schema/fixtures/relay-resolver-implementing-a-field-defined-by-parent-interface.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-implementing-a-field-defined-by-parent-interface.expected");
    test_fixture(transform_fixture, "relay-resolver-implementing-a-field-defined-by-parent-interface.js", "to_schema/fixtures/relay-resolver-implementing-a-field-defined-by-parent-interface.expected", input, expected);
}

#[test]
fn relay_resolver_named_export() {
    let input = include_str!("to_schema/fixtures/relay-resolver-named-export.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-named-export.expected");
    test_fixture(transform_fixture, "relay-resolver-named-export.js", "to_schema/fixtures/relay-resolver-named-export.expected", input, expected);
}

#[test]
fn relay_resolver_on_interface() {
    let input = include_str!("to_schema/fixtures/relay-resolver-on-interface.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-on-interface.expected");
    test_fixture(transform_fixture, "relay-resolver-on-interface.js", "to_schema/fixtures/relay-resolver-on-interface.expected", input, expected);
}

#[test]
fn relay_resolver_on_interface_implementing_a_field_defined_by_parent_interface() {
    let input = include_str!("to_schema/fixtures/relay-resolver-on-interface-implementing-a-field-defined-by-parent-interface.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-on-interface-implementing-a-field-defined-by-parent-interface.expected");
    test_fixture(transform_fixture, "relay-resolver-on-interface-implementing-a-field-defined-by-parent-interface.js", "to_schema/fixtures/relay-resolver-on-interface-implementing-a-field-defined-by-parent-interface.expected", input, expected);
}

#[test]
fn relay_resolver_on_interface_with_type_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-on-interface-with-type.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-on-interface-with-type.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-on-interface-with-type.invalid.js", "to_schema/fixtures/relay-resolver-on-interface-with-type.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_on_invalid_interface_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-on-invalid-interface.invalid.js", "to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_on_invalid_type_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-on-invalid-type.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-on-invalid-type.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-on-invalid-type.invalid.js", "to_schema/fixtures/relay-resolver-on-invalid-type.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_on_type_with_interface_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-on-type-with-interface.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-on-type-with-interface.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-on-type-with-interface.invalid.js", "to_schema/fixtures/relay-resolver-on-type-with-interface.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object.js", "to_schema/fixtures/relay-resolver-strong-object.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_implements() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-implements.js", "to_schema/fixtures/relay-resolver-strong-object-with-implements.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_implements_interface_bad_id_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-bad-id.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-bad-id.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-implements-interface-bad-id.invalid.js", "to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-bad-id.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_implements_interface_no_id_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-no-id.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-no-id.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-implements-interface-no-id.invalid.js", "to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-no-id.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_implements_interface_non_interface() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-non-interface.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-non-interface.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-implements-interface-non-interface.js", "to_schema/fixtures/relay-resolver-strong-object-with-implements-interface-non-interface.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_implements_non_existing_type() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-non-existing-type.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-non-existing-type.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-implements-non-existing-type.js", "to_schema/fixtures/relay-resolver-strong-object-with-implements-non-existing-type.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_implements_server_interface_invalid() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-server-interface.invalid.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-implements-server-interface.invalid.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-implements-server-interface.invalid.js", "to_schema/fixtures/relay-resolver-strong-object-with-implements-server-interface.invalid.expected", input, expected);
}

#[test]
fn relay_resolver_strong_object_with_multiple_implements() {
    let input = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-multiple-implements.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-strong-object-with-multiple-implements.expected");
    test_fixture(transform_fixture, "relay-resolver-strong-object-with-multiple-implements.js", "to_schema/fixtures/relay-resolver-strong-object-with-multiple-implements.expected", input, expected);
}

#[test]
fn relay_resolver_with_args() {
    let input = include_str!("to_schema/fixtures/relay-resolver-with-args.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-with-args.expected");
    test_fixture(transform_fixture, "relay-resolver-with-args.js", "to_schema/fixtures/relay-resolver-with-args.expected", input, expected);
}

#[test]
fn relay_resolver_with_field_and_fragment_args() {
    let input = include_str!("to_schema/fixtures/relay-resolver-with-field-and-fragment-args.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-with-field-and-fragment-args.expected");
    test_fixture(transform_fixture, "relay-resolver-with-field-and-fragment-args.js", "to_schema/fixtures/relay-resolver-with-field-and-fragment-args.expected", input, expected);
}

#[test]
fn relay_resolver_with_field_args() {
    let input = include_str!("to_schema/fixtures/relay-resolver-with-field-args.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-with-field-args.expected");
    test_fixture(transform_fixture, "relay-resolver-with-field-args.js", "to_schema/fixtures/relay-resolver-with-field-args.expected", input, expected);
}

#[test]
fn relay_resolver_with_output_type() {
    let input = include_str!("to_schema/fixtures/relay-resolver-with-output-type.js");
    let expected = include_str!("to_schema/fixtures/relay-resolver-with-output-type.expected");
    test_fixture(transform_fixture, "relay-resolver-with-output-type.js", "to_schema/fixtures/relay-resolver-with-output-type.expected", input, expected);
}

#[test]
fn terse_relay_resolver() {
    let input = include_str!("to_schema/fixtures/terse-relay-resolver.js");
    let expected = include_str!("to_schema/fixtures/terse-relay-resolver.expected");
    test_fixture(transform_fixture, "terse-relay-resolver.js", "to_schema/fixtures/terse-relay-resolver.expected", input, expected);
}

#[test]
fn terse_relay_resolver_id_invalid() {
    let input = include_str!("to_schema/fixtures/terse-relay-resolver-id.invalid.js");
    let expected = include_str!("to_schema/fixtures/terse-relay-resolver-id.invalid.expected");
    test_fixture(transform_fixture, "terse-relay-resolver-id.invalid.js", "to_schema/fixtures/terse-relay-resolver-id.invalid.expected", input, expected);
}

#[test]
fn terse_relay_resolver_interface() {
    let input = include_str!("to_schema/fixtures/terse-relay-resolver-interface.js");
    let expected = include_str!("to_schema/fixtures/terse-relay-resolver-interface.expected");
    test_fixture(transform_fixture, "terse-relay-resolver-interface.js", "to_schema/fixtures/terse-relay-resolver-interface.expected", input, expected);
}

#[test]
fn terse_relay_resolver_non_existent_type_invalid() {
    let input = include_str!("to_schema/fixtures/terse-relay-resolver-non-existent-type.invalid.js");
    let expected = include_str!("to_schema/fixtures/terse-relay-resolver-non-existent-type.invalid.expected");
    test_fixture(transform_fixture, "terse-relay-resolver-non-existent-type.invalid.js", "to_schema/fixtures/terse-relay-resolver-non-existent-type.invalid.expected", input, expected);
}

#[test]
fn terse_relay_resolver_with_output_type() {
    let input = include_str!("to_schema/fixtures/terse-relay-resolver-with-output-type.js");
    let expected = include_str!("to_schema/fixtures/terse-relay-resolver-with-output-type.expected");
    test_fixture(transform_fixture, "terse-relay-resolver-with-output-type.js", "to_schema/fixtures/terse-relay-resolver-with-output-type.expected", input, expected);
}

#[test]
fn terse_relay_resolver_with_root_fragment_on_model() {
    let input = include_str!("to_schema/fixtures/terse-relay-resolver-with-root-fragment-on-model.js");
    let expected = include_str!("to_schema/fixtures/terse-relay-resolver-with-root-fragment-on-model.expected");
    test_fixture(transform_fixture, "terse-relay-resolver-with-root-fragment-on-model.js", "to_schema/fixtures/terse-relay-resolver-with-root-fragment-on-model.expected", input, expected);
}

#[test]
fn terse_resolver_duplicated_invalid() {
    let input = include_str!("to_schema/fixtures/terse-resolver-duplicated.invalid.js");
    let expected = include_str!("to_schema/fixtures/terse-resolver-duplicated.invalid.expected");
    test_fixture(transform_fixture, "terse-resolver-duplicated.invalid.js", "to_schema/fixtures/terse-resolver-duplicated.invalid.expected", input, expected);
}

#[test]
fn weak_type() {
    let input = include_str!("to_schema/fixtures/weak-type.js");
    let expected = include_str!("to_schema/fixtures/weak-type.expected");
    test_fixture(transform_fixture, "weak-type.js", "to_schema/fixtures/weak-type.expected", input, expected);
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::Diagnostic;
use common::FeatureFlag;
use common::SourceLocationKey;
use docblock_syntax::parse_docblock;
use extract_graphql::JavaScriptSourceFeature;
use fixture_tests::Fixture;
use graphql_cli::DiagnosticPrinter;
use graphql_syntax::parse_executable;
use graphql_syntax::ExecutableDefinition;
use intern::string_key::Intern;
use relay_docblock::parse_docblock_ast;
use relay_docblock::ParseOptions;

pub fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String> {
    let js_features = extract_graphql::extract(fixture.content);
    let executable_documents = js_features
        .iter()
        .enumerate()
        .filter_map(|(i, source)| match source {
            JavaScriptSourceFeature::GraphQL(source) => Some(
                parse_executable(
                    &source.text_source().text,
                    SourceLocationKey::Embedded {
                        path: format!("/path/to/test/fixture/{}", fixture.file_name).intern(),
                        index: i as u16,
                    },
                )
                .map_err(|diagnostics| diagnostics_to_sorted_string(fixture.content, &diagnostics))
                .map(|document| document.definitions),
            ),
            JavaScriptSourceFeature::Docblock(_) => None,
        })
        .collect::<Result<Vec<_>, String>>()?
        .iter()
        .flatten()
        .cloned()
        .collect::<Vec<ExecutableDefinition>>();

    let irs = js_features
        .iter()
        .enumerate()
        .filter_map(|(i, source)| match source {
            JavaScriptSourceFeature::GraphQL(_) => None,
            JavaScriptSourceFeature::Docblock(docblock_source) => Some(
                parse_docblock(
                    &docblock_source.text_source().text,
                    SourceLocationKey::Embedded {
                        path: format!("/path/to/test/fixture/{}", fixture.file_name).intern(),
                        index: i as u16,
                    },
                )
                .and_then(|ast| {
                    parse_docblock_ast(
                        &ast,
                        Some(&executable_documents),
                        ParseOptions {
                            enable_output_type: if fixture
                                .content
                                .contains("// relay:enable_output_type")
                            {
                                &FeatureFlag::Enabled
                            } else {
                                &FeatureFlag::Disabled
                            },
                        },
                    )
                })
                .map_err(|diagnostics| diagnostics_to_sorted_string(fixture.content, &diagnostics)),
            ),
        })
        .collect::<Result<Vec<_>, String>>()?;

    let output = irs
        .iter()
        .flatten()
        .map(|ir| format!("{:#?}", ir))
        .collect::<Vec<_>>()
        .join("\n\n");

    Ok(output)
}

pub fn diagnostics_to_sorted_string(source: &str, diagnostics: &[Diagnostic]) -> String {
    let printer = DiagnosticPrinter::new(|source_location| match source_location {
        SourceLocationKey::Embedded { index, .. } => Some(
            extract_graphql::extract(source)[index as usize]
                .text_source()
                .clone(),
        ),
        SourceLocationKey::Standalone { .. } => None,
        SourceLocationKey::Generated => None,
    });
    let mut printed = diagnostics
        .iter()
        .map(|diagnostic| printer.diagnostic_to_string(diagnostic))
        .collect::<Vec<_>>();
    printed.sort();
    printed.join("\n\n")
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::sync::Arc;

use common::DiagnosticsResult;
use common::FeatureFlag;
use common::SourceLocationKey;
use docblock_syntax::parse_docblock;
use docblock_syntax::DocblockSource;
use extract_graphql::JavaScriptSourceFeature;
use fixture_tests::Fixture;
use graphql_syntax::parse_executable;
use graphql_syntax::ExecutableDefinition;
use graphql_test_helpers::diagnostics_to_sorted_string;
use intern::string_key::Intern;
use relay_docblock::extend_schema_with_resolver_type_system_definition;
use relay_docblock::parse_docblock_ast;
use relay_docblock::ParseOptions;
use relay_test_schema::get_test_schema_with_extensions;
use schema::SDLSchema;

pub fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String> {
    let parts: Vec<_> = fixture.content.split("%extensions%").collect();
    let (base, mut schema) = match parts.as_slice() {
        [base, extensions] => (base, extract_schema_from_js(extensions)),
        [base] => (base, get_test_schema_with_extensions("")),
        _ => panic!("Invalid fixture input {}", fixture.content),
    };

    let js_features = extract_graphql::extract(base);

    let executable_documents = js_features
        .iter()
        .enumerate()
        .filter_map(|(_, source)| match source {
            JavaScriptSourceFeature::GraphQL(source) => Some(
                parse_executable(&source.text_source().text, SourceLocationKey::Generated)
                    .map_err(|diagnostics| {
                        diagnostics_to_sorted_string(&source.text_source().text, &diagnostics)
                    })
                    .map(|document| document.definitions),
            ),
            JavaScriptSourceFeature::Docblock(_) => None,
        })
        .collect::<Result<Vec<_>, String>>()?
        .iter()
        .flatten()
        .cloned()
        .collect::<Vec<ExecutableDefinition>>();

    let mut stringify = |i: usize, source: &DocblockSource| -> DiagnosticsResult<String> {
        let ast = parse_docblock(
            &source.text_source().text,
            SourceLocationKey::Embedded {
                path: format!("/path/to/test/fixture/{}", fixture.file_name).intern(),
                index: i as u16,
            },
        )?;
        let ir = parse_docblock_ast(
            &ast,
            Some(&executable_documents),
            ParseOptions {
                enable_output_type: if fixture.content.contains("// relay:enable_output_type") {
                    &FeatureFlag::Enabled
                } else {
                    &FeatureFlag::Disabled
                },
            },
        )?
        .unwrap();

        // In non-tests, this function (correctly) consumes TypeSystemDefinition when modifying the
        // schema.
        // In tests, we need to clone, because we **also** want to print the schema changes.
        let schema_document = ir
            .clone()
            .to_graphql_schema_ast(&schema, &Default::default())?;
        for definition in &schema_document.definitions {
            extend_schema_with_resolver_type_system_definition(
                definition.clone(),
                Arc::get_mut(&mut schema)
                    .expect("Expected to be able to get mutable reference to schema"),
                schema_document.location,
            )?;
        }

        ir.to_sdl_string(&schema, &Default::default())
    };

    let schema_strings = js_features
        .iter()
        .enumerate()
        .filter_map(|(i, source)| match source {
            JavaScriptSourceFeature::GraphQL(_) => None,
            JavaScriptSourceFeature::Docblock(docblock_source) => Some((i, docblock_source)),
        })
        .map(|(i, source)| {
            stringify(i, source).map_err(|diagnostics| {
                diagnostics_to_sorted_string(&source.text_source().text, &diagnostics)
            })
        })
        .collect::<Result<Vec<_>, String>>()?;

    Ok(schema_strings.join("\n\n"))
}

fn extract_schema_from_js(js: &str) -> Arc<SDLSchema> {
    let js_features = extract_graphql::extract(js);
    let sdl_text = match js_features.as_slice() {
        [JavaScriptSourceFeature::GraphQL(source)] => &source.text_source().text,
        _ => {
            panic!("Expected %extensions% to contain exactly 1 graphql`` tagged template literal.")
        }
    };

    get_test_schema_with_extensions(sdl_text)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! An LSP client implementation, currently used only for testing
#[cfg(test)]
use lsp_server::Connection;
#[cfg(test)]
use lsp_server::Message;
#[cfg(test)]
use lsp_server::Notification;
#[cfg(test)]
use lsp_server::Request as ServerRequest;
#[cfg(test)]
use lsp_server::RequestId;
#[cfg(test)]
use lsp_types::request::Initialize;
#[cfg(test)]
use lsp_types::request::Request;
#[cfg(test)]
use lsp_types::InitializeParams;
#[cfg(test)]
use lsp_types::InitializedParams;

#[cfg(test)]
pub fn initialize(client: &Connection, params: &InitializeParams, request_id: u64) {
    let request_id = RequestId::from(request_id.to_string());
    let request = ServerRequest {
        id: request_id,
        method: Initialize::METHOD.to_string(),
        params: serde_json::to_value(params).unwrap(),
    };
    let msg = Message::Request(request);
    client.sender.send(msg).unwrap();
    let notification = Message::Notification(Notification {
        method: "initialized".to_string(),
        params: serde_json::to_value(InitializedParams {}).unwrap(),
    });
    client.sender.send(notification).unwrap();
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! Utilities for reporting errors to an LSP client
use std::path::PathBuf;

use common::get_diagnostics_data;
use common::Diagnostic as CompilerDiagnostic;
use common::DiagnosticRelatedInformation;
use common::Location;
use common::TextSource;
use crossbeam::channel::Sender;
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use dunce::canonicalize;
use extract_graphql::JavaScriptSourceFeature;
use lsp_server::Message;
use lsp_server::Notification as ServerNotification;
use lsp_types::notification::Notification;
use lsp_types::notification::PublishDiagnostics;
use lsp_types::Diagnostic;
use lsp_types::DiagnosticRelatedInformation as LspDiagnosticRelatedInformation;
use lsp_types::DiagnosticSeverity;
use lsp_types::DiagnosticTag;
use lsp_types::Location as LspLocation;
use lsp_types::Position;
use lsp_types::PublishDiagnosticsParams;
use lsp_types::Range;
use lsp_types::Url;
use relay_compiler::errors::BuildProjectError;
use relay_compiler::errors::Error;
use relay_compiler::source_for_location;
use relay_compiler::FsSourceReader;
use relay_compiler::SourceReader;

use crate::lsp_process_error::LSPProcessResult;

/// Converts a Location to a Url pointing to the canonical path based on the root_dir provided.
/// Returns None if we are unable to do the conversion
fn url_from_location(location: Location, root_dir: &PathBuf) -> Option<Url> {
    let file_path = location.source_location().path();
    let canonical_path = canonicalize(root_dir.join(file_path)).ok()?;
    Url::from_file_path(canonical_path).ok()
}

#[derive(Default)]
struct DiagnosticSet {
    /// Stores the diagnostics from IDE source, which will be updated on any text change
    quick_diagnostics: Vec<Diagnostic>,
    /// Stores the diagnostics from watchman source, which will be updated on file save
    regular_diagnostics: Vec<Diagnostic>,
}

pub struct DiagnosticReporter {
    active_diagnostics: DashMap<Url, DiagnosticSet>,
    sender: Sender<Message>,
    root_dir: PathBuf,
    source_reader: Box<dyn SourceReader + Send + Sync>,
}

impl DiagnosticReporter {
    pub fn new(root_dir: PathBuf, sender: Sender<Message>) -> Self {
        Self {
            active_diagnostics: Default::default(),
            sender,
            root_dir,
            source_reader: Box::new(FsSourceReader),
        }
    }

    pub fn clear_regular_diagnostics(&self) {
        for mut r in self.active_diagnostics.iter_mut() {
            let (url, diagnostics) = r.pair_mut();
            diagnostics.regular_diagnostics.clear();
            self.publish_diagnostics_set(url, diagnostics);
        }
        self.active_diagnostics.retain(|_, diagnostics| {
            !diagnostics.regular_diagnostics.is_empty() || !diagnostics.quick_diagnostics.is_empty()
        })
    }

    pub fn report_error(&self, error: &Error) {
        match error {
            Error::DiagnosticsError { errors } => {
                for diagnostic in errors {
                    self.report_diagnostic(diagnostic);
                }
            }
            Error::BuildProjectsErrors { errors } => {
                for error in errors {
                    self.print_project_error(error);
                }
            }
            Error::Cancelled => {
                // Ignore the cancellation
            }
            error => {
                self.print_generic_error(format!("{}", error));
            }
        }
    }

    pub fn report_diagnostics(&self, diagnostics: &[common::Diagnostic]) {
        for diagnostic in diagnostics {
            self.report_diagnostic(diagnostic);
        }
    }

    pub fn commit_diagnostics(&self) {
        for r in self.active_diagnostics.iter() {
            let (url, diagnostics) = r.pair();
            self.publish_diagnostics_set(url, diagnostics)
        }
    }

    fn add_diagnostic(&self, url: Url, diagnostic: Diagnostic) {
        self.active_diagnostics
            .entry(url)
            .or_default()
            .regular_diagnostics
            .push(diagnostic);
    }

    pub fn update_quick_diagnostics_for_url(&self, url: &Url, diagnostics: Vec<Diagnostic>) {
        match self.active_diagnostics.entry(url.clone()) {
            Entry::Occupied(mut e) => {
                let data = e.get_mut();
                if data.quick_diagnostics != diagnostics {
                    data.quick_diagnostics = diagnostics;
                    self.publish_diagnostics_set(url, data);
                }
            }
            Entry::Vacant(e) => {
                if !diagnostics.is_empty() {
                    let data = DiagnosticSet {
                        regular_diagnostics: vec![],
                        quick_diagnostics: diagnostics,
                    };
                    self.publish_diagnostics_set(url, &data);
                    e.insert(data);
                }
            }
        }
    }

    pub fn clear_quick_diagnostics_for_url(&self, url: &Url) {
        if let Some(mut diagnostics) = self.active_diagnostics.get_mut(url) {
            if !diagnostics.quick_diagnostics.is_empty() {
                diagnostics.quick_diagnostics.clear();
                self.publish_diagnostics_set(url, &diagnostics)
            }
        }
    }

    fn publish_diagnostics_set(&self, url: &Url, diagnostics: &DiagnosticSet) {
        let mut next_diagnostics = diagnostics.quick_diagnostics.clone();
        for diagnostic in &diagnostics.regular_diagnostics {
            if !next_diagnostics
                .iter()
                .any(|prev_diag| prev_diag.eq(diagnostic))
            {
                next_diagnostics.push(diagnostic.clone());
            }
        }
        let params = PublishDiagnosticsParams {
            diagnostics: next_diagnostics,
            uri: url.clone(),
            version: None,
        };
        publish_diagnostic(params, &self.sender).ok();
    }

    #[cfg(test)]
    fn set_source_reader(&mut self, source_reader: Box<dyn SourceReader + Send + Sync>) {
        self.source_reader = source_reader;
    }

    fn report_diagnostic(&self, diagnostic: &CompilerDiagnostic) {
        let location = diagnostic.location();

        let url = match self.url_from_location(location) {
            Some(url) => url,
            None => return,
        };
        let source = match self.source_from_location(location) {
            Some(source) => source,
            None => return,
        };

        let diagnostic = self.convert_diagnostic(source.text_source(), diagnostic);
        self.add_diagnostic(url, diagnostic);
    }

    pub fn convert_diagnostic(
        &self,
        text_source: &TextSource,
        diagnostic: &CompilerDiagnostic,
    ) -> Diagnostic {
        let tags: Vec<DiagnosticTag> = diagnostic.tags();

        let related_information = diagnostic
            .related_information()
            .iter()
            .filter_map(|info| self.maybe_convert_related_information(info))
            .collect::<Vec<_>>();

        Diagnostic {
            code: None,
            data: get_diagnostics_data(diagnostic),
            message: diagnostic.message().to_string(),
            range: text_source.to_span_range(diagnostic.location().span()),
            related_information: if related_information.is_empty() {
                None
            } else {
                Some(related_information)
            },
            severity: Some(diagnostic.severity()),
            tags: if tags.is_empty() { None } else { Some(tags) },
            source: None,
            ..Default::default()
        }
    }

    pub fn maybe_convert_related_information(
        &self,
        info: &DiagnosticRelatedInformation,
    ) -> Option<LspDiagnosticRelatedInformation> {
        let uri = self.url_from_location(info.location)?;
        let source = self.source_from_location(info.location)?;

        Some(LspDiagnosticRelatedInformation {
            message: info.message.to_string(),
            location: LspLocation {
                range: source.text_source().to_span_range(info.location.span()),
                uri,
            },
        })
    }

    pub fn url_from_location(&self, location: Location) -> Option<Url> {
        url_from_location(location, &self.root_dir).or_else(|| {
            // TODO(brandondail) we should always be able to parse as a Url, so log here when we don't
            Url::from_directory_path(&self.root_dir).ok()
        })
    }

    fn source_from_location(&self, location: Location) -> Option<JavaScriptSourceFeature> {
        source_for_location(
            &self.root_dir,
            location.source_location(),
            self.source_reader.as_ref(),
        )
    }

    fn print_project_error(&self, error: &BuildProjectError) {
        match error {
            BuildProjectError::ValidationErrors { errors, .. } => {
                for diagnostic in errors {
                    self.report_diagnostic(diagnostic);
                }
            }
            BuildProjectError::PersistErrors { errors, .. } => {
                for error in errors {
                    self.print_generic_error(format!("{}", error));
                }
            }
            _ => {
                self.print_generic_error(format!("{}", error));
            }
        }
    }

    fn print_generic_error(&self, message: String) {
        let diagnostic = Diagnostic {
            code: None,
            message,
            range: Range::new(Position::new(0, 0), Position::new(0, 0)),
            related_information: None,
            severity: Some(DiagnosticSeverity::ERROR),
            source: None,
            tags: None,
            ..Default::default()
        };
        let url = Url::from_directory_path(&self.root_dir)
            .expect("print_generic_error: Could not convert self.root_dir to Url");
        self.add_diagnostic(url, diagnostic);
    }

    pub fn get_diagnostics_for_range(&self, url: &Url, range: Range) -> Option<Diagnostic> {
        let diagnostic_set = self.active_diagnostics.get(url)?;
        diagnostic_set
            .quick_diagnostics
            .iter()
            .find(|item| is_sub_range(range, item.range))
            .or_else(|| {
                diagnostic_set
                    .regular_diagnostics
                    .iter()
                    .find(|item| is_sub_range(range, item.range))
            })
            .cloned()
    }
}

/// Checks if `inner` range is within the `outer` range.
/// First, we need to make sure that the start character of the outer range
/// is before (or the same) character of the inner range.
/// Then we need to make sure that end character of the outer range is after
/// (or the same) as the end character of the inner range, or outer line
/// is more than inner end line.
fn is_sub_range(inner: Range, outer: Range) -> bool {
    (outer.start.character <= inner.start.character && outer.start.line <= inner.start.line)
        && (outer.end.character >= inner.end.character && outer.end.line >= inner.end.line)
}

/// Publish diagnostics to the client
pub fn publish_diagnostic(
    diagnostic_params: PublishDiagnosticsParams,
    sender: &Sender<Message>,
) -> LSPProcessResult<()> {
    let notif = ServerNotification::new(PublishDiagnostics::METHOD.into(), diagnostic_params);
    sender
        .send(Message::Notification(notif))
        .unwrap_or_else(|_| {
            // TODO(brandondail) log here
        });
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::env;
    use std::path::PathBuf;

    use common::Diagnostic;
    use common::Location;
    use common::SourceLocationKey;
    use common::Span;
    use intern::string_key::Intern;
    use lsp_types::Position;
    use lsp_types::Range;
    use relay_compiler::SourceReader;

    use super::is_sub_range;
    use super::DiagnosticReporter;

    struct MockSourceReader(String);

    impl SourceReader for MockSourceReader {
        fn read_file_to_string(&self, _path: &PathBuf) -> std::io::Result<String> {
            Ok(self.0.to_string())
        }
    }

    #[test]
    fn report_diagnostic_test() {
        let root_dir =
            env::current_dir().expect("expect to be able to get the current working directory");
        let (sender, _) = crossbeam::channel::unbounded();
        let mut reporter = DiagnosticReporter::new(root_dir, sender);
        reporter.set_source_reader(Box::new(MockSourceReader("Content".to_string())));
        let source_location = SourceLocationKey::Standalone {
            path: "foo.txt".intern(),
        };
        assert_eq!(reporter.active_diagnostics.len(), 0);
        reporter.report_diagnostic(&Diagnostic::error(
            "test message",
            Location::new(source_location, Span { start: 0, end: 1 }),
        ));
        assert_eq!(reporter.active_diagnostics.len(), 1);
    }

    /// This test will assert that the message without URL (with generated source) won't be reported by LSPStatusReporter
    /// I'm not sure if this is the right behavior, but lets capture it here.
    #[test]
    fn do_not_report_diagnostic_without_url_test() {
        let root_dir = PathBuf::from("/tmp");
        let (sender, _) = crossbeam::channel::unbounded();

        let mut reporter = DiagnosticReporter::new(root_dir, sender);
        reporter.set_source_reader(Box::new(MockSourceReader("".to_string())));

        reporter.report_diagnostic(&Diagnostic::error("-", Location::generated()));
        assert_eq!(reporter.active_diagnostics.len(), 0);
    }

    #[test]
    fn sub_range_inner_directly_below() {
        let cursor = Range::new(Position::new(106, 12), Position::new(106, 12));
        let diagnostic = Range::new(Position::new(92, 6), Position::new(92, 17));
        assert!(!is_sub_range(cursor, diagnostic));
    }

    #[test]
    fn sub_range_inner_directly_above() {
        let cursor = Range::new(Position::new(80, 12), Position::new(80, 12));
        let diagnostic = Range::new(Position::new(92, 6), Position::new(92, 17));
        assert!(!is_sub_range(cursor, diagnostic));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::Span;
use graphql_ir::reexport::StringKey;
use graphql_ir::FragmentDefinitionName;
use relay_docblock::DocblockIr;
use relay_docblock::On;

pub enum DocblockResolutionInfo {
    Type(StringKey),
    RootFragment(FragmentDefinitionName),
    FieldName(StringKey),
    Deprecated,
}

pub fn create_docblock_resolution_info(
    docblock_ir: &DocblockIr,
    position_span: Span,
) -> Option<DocblockResolutionInfo> {
    match docblock_ir {
        DocblockIr::RelayResolver(resolver_ir) => {
            match resolver_ir.on {
                On::Type(on_type) => {
                    if on_type.value.location.contains(position_span) {
                        return Some(DocblockResolutionInfo::Type(on_type.value.item));
                    }
                }
                On::Interface(on_interface) => {
                    if on_interface.value.location.contains(position_span) {
                        return Some(DocblockResolutionInfo::Type(on_interface.value.item));
                    }
                }
            };

            if let Some(root_fragment) = resolver_ir.root_fragment {
                if root_fragment.location.contains(position_span) {
                    return Some(DocblockResolutionInfo::RootFragment(root_fragment.item));
                }
            }

            if resolver_ir.field.name.span.contains(position_span) {
                return Some(DocblockResolutionInfo::FieldName(
                    resolver_ir.field.name.value,
                ));
            }

            if let Some(output_type) = &resolver_ir.output_type {
                if output_type.inner().location.contains(position_span) {
                    return Some(DocblockResolutionInfo::Type(
                        output_type.inner().item.inner().name.value,
                    ));
                }
            }

            if let Some(deprecated) = resolver_ir.deprecated {
                if deprecated.key_location().contains(position_span) {
                    return Some(DocblockResolutionInfo::Deprecated);
                }
            }

            None
        }
        DocblockIr::TerseRelayResolver(resolver_ir) => {
            // Parent type
            if resolver_ir.type_.location.contains(position_span) {
                return Some(DocblockResolutionInfo::Type(resolver_ir.type_.item));
            }

            let field_type_location = resolver_ir
                .location
                .with_span(resolver_ir.field.type_.span());

            // Return type
            if field_type_location.contains(position_span) {
                return Some(DocblockResolutionInfo::Type(
                    resolver_ir.field.type_.inner().name.value,
                ));
            }

            // Root fragment
            if let Some(root_fragment) = resolver_ir.root_fragment {
                if root_fragment.location.contains(position_span) {
                    return Some(DocblockResolutionInfo::RootFragment(root_fragment.item));
                }
            }

            // @deprecated key
            if let Some(deprecated) = resolver_ir.deprecated {
                if deprecated.key_location().contains(position_span) {
                    return Some(DocblockResolutionInfo::Deprecated);
                }
            }

            None
        }
        DocblockIr::StrongObjectResolver(strong_object) => {
            if strong_object.rhs_location.contains(position_span) {
                return Some(DocblockResolutionInfo::Type(strong_object.type_name.value));
            }

            if let Some(deprecated) = strong_object.deprecated {
                if deprecated.key_location().contains(position_span) {
                    return Some(DocblockResolutionInfo::Deprecated);
                }
            }
            None
        }
        DocblockIr::WeakObjectType(weak_type_ir) => {
            if weak_type_ir.rhs_location.contains(position_span) {
                return Some(DocblockResolutionInfo::Type(weak_type_ir.type_name.value));
            }

            if let Some(deprecated) = weak_type_ir.deprecated {
                if deprecated.key_location().contains(position_span) {
                    return Some(DocblockResolutionInfo::Deprecated);
                }
            }
            // TODO: We could provide location mapping for the @weak docblock attribute
            None
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use lsp_types::request::CodeActionRequest;
use lsp_types::request::Completion;
use lsp_types::request::Request;
use lsp_types::Url;

use crate::lsp_runtime_error::LSPRuntimeResult;

/// Interface for the LSP server to handle JavaScript text
pub trait JSLanguageServer: Send + Sync {
    type TState;

    fn process_js_source(&self, url: &Url, text: &str);
    fn remove_js_source(&self, url: &Url);
    fn on_complete(
        &self,
        params: &<Completion as Request>::Params,
        state: &Self::TState,
    ) -> LSPRuntimeResult<<Completion as Request>::Result>;
    fn on_code_action(
        &self,
        params: &<CodeActionRequest as Request>::Params,
        state: &Self::TState,
    ) -> LSPRuntimeResult<<CodeActionRequest as Request>::Result>;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#![deny(clippy::all)]

mod client;
mod code_action;
pub mod completion;
pub mod diagnostic_reporter;
mod docblock_resolution_info;
mod explore_schema_for_type;
pub mod find_field_usages;
pub mod goto_definition;
mod graphql_tools;
pub mod hover;
pub mod js_language_server;
pub mod location;
mod lsp_extra_data_provider;
pub mod lsp_process_error;
pub mod lsp_runtime_error;
pub mod node_resolution_info;
pub mod references;
mod resolved_types_at_location;
mod search_schema_items;
mod server;
mod shutdown;
mod status_reporter;
pub mod status_updater;
pub mod text_documents;
pub mod utils;
use std::sync::Arc;

use common::PerfLogger;
use docblock_resolution_info::DocblockResolutionInfo;
pub use extract_graphql::JavaScriptSourceFeature;
use graphql_syntax::ExecutableDocument;
pub use hover::ContentConsumerType;
pub use js_language_server::JSLanguageServer;
use log::debug;
pub use lsp_extra_data_provider::DummyExtraDataProvider;
pub use lsp_extra_data_provider::FieldDefinitionSourceInfo;
pub use lsp_extra_data_provider::FieldSchemaInfo;
pub use lsp_extra_data_provider::LSPExtraDataProvider;
use lsp_process_error::LSPProcessResult;
pub use lsp_runtime_error::LSPRuntimeError;
pub use lsp_runtime_error::LSPRuntimeResult;
use lsp_server::Connection;
use node_resolution_info::NodeResolutionInfo;
use relay_compiler::config::Config;
use relay_docblock::DocblockIr;
use schema_documentation::SchemaDocumentation;
use schema_documentation::SchemaDocumentationLoader;
pub use server::GlobalState;
pub use server::LSPNotificationDispatch;
pub use server::LSPRequestDispatch;
pub use server::LSPState;
pub use server::Schemas;
pub use utils::position_to_offset;

pub enum Feature {
    GraphQLDocument(ExecutableDocument),
    DocblockIr(DocblockIr),
}

#[allow(clippy::large_enum_variant)]
pub enum FeatureResolutionInfo {
    GraphqlNode(NodeResolutionInfo),
    DocblockNode(DocblockNode),
}

pub struct DocblockNode {
    resolution_info: DocblockResolutionInfo,
    ir: DocblockIr,
}

pub async fn start_language_server<
    TPerfLogger,
    TSchemaDocumentation: SchemaDocumentation + 'static,
>(
    config: Config,
    perf_logger: Arc<TPerfLogger>,
    extra_data_provider: Box<dyn LSPExtraDataProvider + Send + Sync>,
    schema_documentation_loader: Option<Box<dyn SchemaDocumentationLoader<TSchemaDocumentation>>>,
    js_language_server: Option<
        Box<dyn JSLanguageServer<TState = LSPState<TPerfLogger, TSchemaDocumentation>>>,
    >,
) -> LSPProcessResult<()>
where
    TPerfLogger: PerfLogger + 'static,
{
    let (connection, io_handles) = Connection::stdio();
    debug!("Initialized stdio transport layer");
    let params = server::initialize(&connection)?;
    debug!("JSON-RPC handshake completed");
    server::run(
        connection,
        config,
        params,
        perf_logger,
        extra_data_provider,
        schema_documentation_loader,
        js_language_server,
    )
    .await?;
    io_handles.join()?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use lsp_server::Connection;
    use lsp_types::ClientCapabilities;
    use lsp_types::InitializeParams;

    use super::client;
    use super::lsp_process_error::LSPProcessResult;
    use super::server;
    #[test]
    fn initialize() -> LSPProcessResult<()> {
        // Test with an in-memory connection pair
        let (connection, client) = Connection::memory();
        // Mock set of client parameters. The `root_path` field is deprecated, but
        // still required to construct the params, so we allow deprecated fields here.
        #[allow(deprecated)]
        let init_params = InitializeParams {
            process_id: Some(1),
            root_path: None,
            root_uri: None,
            initialization_options: None,
            capabilities: ClientCapabilities::default(),
            trace: None,
            workspace_folders: None,
            client_info: None,
            locale: None,
        };
        client::initialize(&client, &init_params, 0);
        let params = server::initialize(&connection)?;
        assert_eq!(params, init_params);
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::path::Path;
use std::path::PathBuf;

use common::Location;
use common::SourceLocationKey;
use common::TextSource;
use intern::Lookup;
use lsp_types::Url;

use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;

/// Given a root dir and a common::Location, return a Result containing an
/// LSPLocation (i.e. lsp_types::Location).
pub fn transform_relay_location_to_lsp_location(
    root_dir: &Path,
    location: Location,
) -> LSPRuntimeResult<lsp_types::Location> {
    match location.source_location() {
        SourceLocationKey::Standalone { path } => {
            let abspath = root_dir.join(PathBuf::from(path.lookup()));

            let file_contents = get_file_contents(&abspath)?;

            let uri = get_uri(&abspath)?;

            let range =
                TextSource::from_whole_document(file_contents).to_span_range(location.span());
            Ok(lsp_types::Location { uri, range })
        }
        SourceLocationKey::Embedded { path, index } => {
            let path_to_fragment = root_dir.join(PathBuf::from(path.lookup()));
            let uri = get_uri(&path_to_fragment)?;

            let file_contents = get_file_contents(&path_to_fragment)?;

            let response = extract_graphql::extract(&file_contents);
            let response_length = response.len();
            let embedded_source = response
                .into_iter()
                .nth(index.try_into().unwrap())
                .ok_or_else(|| {
                    LSPRuntimeError::UnexpectedError(format!(
                        "File {:?} does not contain enough graphql literals: {} needed; {} found",
                        path_to_fragment, index, response_length
                    ))
                })?;

            let text_source = embedded_source.text_source();
            let range = text_source.to_span_range(location.span());
            Ok(lsp_types::Location { uri, range })
        }
        _ => Err(LSPRuntimeError::UnexpectedError(
            "Cannot get location of generated field in graphql file".to_string(),
        )),
    }
}

fn get_file_contents(path: &Path) -> LSPRuntimeResult<String> {
    let file = std::fs::read(&path).map_err(|e| LSPRuntimeError::UnexpectedError(e.to_string()))?;
    String::from_utf8(file).map_err(|e| LSPRuntimeError::UnexpectedError(e.to_string()))
}

fn get_uri(path: &PathBuf) -> LSPRuntimeResult<Url> {
    Url::parse(&format!(
        "file://{}",
        path.to_str()
            .ok_or_else(|| LSPRuntimeError::UnexpectedError(format!(
                "Could not cast path {:?} as string",
                path
            )))?
    ))
    .map_err(|e| LSPRuntimeError::UnexpectedError(e.to_string()))
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use serde::Deserialize;
use serde::Serialize;

#[derive(Deserialize, Serialize)]
pub struct FieldDefinitionSourceInfo {
    pub file_path: String,
    pub line_number: u64,
    pub is_local: bool,
}

pub struct FieldSchemaInfo {
    pub name: String,
    pub is_extension: bool,
}

/// Interface for the LSP server to handle external data sources
pub trait LSPExtraDataProvider: Send + Sync {
    fn fetch_query_stats(&self, search_token: &str) -> Vec<String>;
    fn resolve_field_definition(
        &self,
        project_name: String,
        parent_type: String,
        field_info: Option<FieldSchemaInfo>,
    ) -> Result<Option<FieldDefinitionSourceInfo>, String>;
}

pub struct DummyExtraDataProvider {}

impl DummyExtraDataProvider {
    pub fn new() -> DummyExtraDataProvider {
        DummyExtraDataProvider {}
    }
}

impl Default for DummyExtraDataProvider {
    fn default() -> Self {
        Self::new()
    }
}

impl LSPExtraDataProvider for DummyExtraDataProvider {
    fn fetch_query_stats(&self, _search_token: &str) -> Vec<String> {
        vec![]
    }

    fn resolve_field_definition(
        &self,
        _project_name: String,
        _parent_type: String,
        _field_info: Option<FieldSchemaInfo>,
    ) -> Result<Option<FieldDefinitionSourceInfo>, String> {
        Ok(None)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::io::Error as IOError;

use crossbeam::channel::SendError;
use lsp_server::Message;
use lsp_server::ProtocolError;
use relay_compiler::errors::Error as CompilerError;
use serde_json::Error as SerdeError;
use tokio::task::JoinError;

pub type LSPProcessResult<T> = std::result::Result<T, LSPProcessError>;

macro_rules! extend_error {
    ($error: ident) => {
        impl From<$error> for LSPProcessError {
            fn from(err: $error) -> Self {
                LSPProcessError::$error(err)
            }
        }
    };
}

#[derive(Debug)]
pub enum LSPProcessError {
    ProtocolError(ProtocolError),
    CompilerError(CompilerError),
    IOError(IOError),
    SerdeError(SerdeError),
    JoinError(JoinError),
    SendError(SendError<Message>),
}

extend_error!(CompilerError);
extend_error!(IOError);
extend_error!(ProtocolError);
extend_error!(SerdeError);
extend_error!(JoinError);

impl From<SendError<Message>> for LSPProcessError {
    fn from(err: SendError<Message>) -> Self {
        LSPProcessError::SendError(err)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use lsp_server::ErrorCode;
use lsp_server::ResponseError;

pub type LSPRuntimeResult<T> = std::result::Result<T, LSPRuntimeError>;

#[derive(Debug, Clone)]
pub enum LSPRuntimeError {
    ExpectedError,
    UnexpectedError(String),
}

impl From<LSPRuntimeError> for Option<ResponseError> {
    fn from(err: LSPRuntimeError) -> Self {
        match err {
            LSPRuntimeError::ExpectedError => None,
            LSPRuntimeError::UnexpectedError(message) => Some(ResponseError {
                code: ErrorCode::UnknownErrorCode as i32,
                message,
                data: None,
            }),
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use lsp_types::notification::Exit;
use lsp_types::notification::Notification;
use lsp_types::request::Request;
use lsp_types::request::Shutdown;

use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;

pub(crate) fn on_shutdown(
    _state: &impl GlobalState,
    _params: <Shutdown as Request>::Params,
) -> LSPRuntimeResult<<Shutdown as Request>::Result> {
    LSPRuntimeResult::Ok(())
}

pub(crate) fn on_exit(
    _state: &impl GlobalState,
    _params: <Exit as Notification>::Params,
) -> LSPRuntimeResult<()> {
    std::process::exit(0);
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! Utilities for reporting errors to an LSP client
use std::path::PathBuf;

use crossbeam::channel::Sender;
use log::info;
use lsp_server::Message;
use relay_compiler::errors::Error;
use relay_compiler::status_reporter::StatusReporter;

use crate::diagnostic_reporter::DiagnosticReporter;
use crate::status_updater::set_error_status;
use crate::status_updater::set_ready_status;
use crate::status_updater::update_in_progress_status;

pub struct LSPStatusReporter {
    diagnostic_reporter: DiagnosticReporter,
    sender: Sender<Message>,
}

impl LSPStatusReporter {
    pub fn new(root_dir: PathBuf, sender: Sender<Message>) -> Self {
        let sender_clone = sender.clone();
        Self {
            sender,
            diagnostic_reporter: DiagnosticReporter::new(root_dir, sender_clone),
        }
    }
}

impl StatusReporter for LSPStatusReporter {
    fn build_starts(&self) {
        update_in_progress_status(
            "Relay: compiling...",
            Some("The Relay extension is checking for errors."),
            &self.sender,
        );
    }

    fn build_completes(&self, diagnostics: &[common::Diagnostic]) {
        set_ready_status(&self.sender);
        self.diagnostic_reporter.clear_regular_diagnostics();
        self.diagnostic_reporter.report_diagnostics(diagnostics);
        self.diagnostic_reporter.commit_diagnostics();
        info!("Compilation completed.");
    }

    fn build_errors(&self, error: &Error) {
        match error {
            Error::BuildProjectsErrors { .. } | Error::DiagnosticsError { .. } => {
                set_ready_status(&self.sender);
                self.diagnostic_reporter.clear_regular_diagnostics();
                self.diagnostic_reporter.report_error(error);
                self.diagnostic_reporter.commit_diagnostics();
            }
            Error::Cancelled => {}
            _ => set_error_status(&self.sender, error),
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! Utilities for reading, writing, and transforming data for the LSP service
// We use two crates, lsp_types and lsp_server, for interacting with LSP. This module re-exports
// types from both so that we have a central source-of-truth for all LSP-related utilities.

use crossbeam::channel::SendError;
use crossbeam::channel::Sender;
use lsp_server::Message;
use lsp_server::Notification as ServerNotification;
use lsp_server::Request as ServerRequest;
use lsp_types::notification::Notification;
use lsp_types::notification::ShowMessage;
use lsp_types::request::Request;
use lsp_types::MessageActionItem;
use lsp_types::MessageType;
use lsp_types::ShowMessageParams;
use serde::Deserialize;
use serde::Serialize;

#[derive(Debug)]
enum ShowStatus {}

#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct ShowStatusParams {
    #[serde(rename = "type")]
    type_: MessageType,
    #[serde(skip_serializing_if = "Option::is_none")]
    progress: Option<Progress>,
    #[serde(skip_serializing_if = "Option::is_none")]
    uri: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    short_message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    actions: Option<Vec<MessageActionItem>>,
}

#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
struct Progress {
    numerator: f32,
    denominator: Option<f32>,
}

impl Request for ShowStatus {
    type Params = ShowStatusParams;
    type Result = ();
    const METHOD: &'static str = "window/showStatus";
}

pub(crate) fn set_ready_status(sender: &Sender<Message>) {
    update_status(
        "Relay: ready",
        Some("The Relay extension is ready"),
        MessageType::INFO,
        sender,
    );
}

pub(crate) fn set_error_status(sender: &Sender<Message>, error: impl std::fmt::Display) {
    update_status(
        "Relay: error",
        Some(format!(
            "The Relay extension has errors: {}. Try reloading the IDE. If the error persists, report the issue via appropriate channels.",
            error
        )),
        MessageType::ERROR,
        sender,
    );
}

pub(crate) fn update_in_progress_status(
    short_message: impl Into<String>,
    message: Option<impl Into<String>>,
    sender: &Sender<Message>,
) {
    update_status(short_message, message, MessageType::WARNING, sender);
}

// This is the entrypoint for all status bar update commands.
// If you change this, please be sure to update the appropriate
// logic in the following file.
// https://github.com/facebook/relay/blob/main/vscode-extension/src/statusBar.ts
pub fn update_status(
    short_message: impl Into<String>,
    message: Option<impl Into<String>>,
    type_: MessageType,
    sender: &Sender<Message>,
) {
    let request = ServerRequest::new(
        ShowStatus::METHOD.to_string().into(),
        ShowStatus::METHOD.into(),
        ShowStatusParams {
            type_,
            progress: None,
            uri: None,
            message: message.map(|s| s.into()),
            short_message: Some(short_message.into()),
            actions: None,
        },
    );
    sender
        .send(Message::Request(request))
        .expect("update_status: failed to send");
}

/// Show a notification in the client
#[allow(dead_code)]
pub(crate) fn show_info_message(
    sender: &Sender<Message>,
    message: impl Into<String>,
) -> Result<(), SendError<Message>> {
    let notif = ServerNotification::new(
        ShowMessage::METHOD.into(),
        ShowMessageParams {
            typ: MessageType::INFO,
            message: message.into(),
        },
    );

    sender.send(Message::Notification(notif))
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! Utilities related to LSP text document syncing

use lsp_types::notification::Cancel;
use lsp_types::notification::DidChangeTextDocument;
use lsp_types::notification::DidCloseTextDocument;
use lsp_types::notification::DidOpenTextDocument;
use lsp_types::notification::DidSaveTextDocument;
use lsp_types::notification::Notification;
use lsp_types::DidChangeTextDocumentParams;
use lsp_types::DidOpenTextDocumentParams;
use lsp_types::TextDocumentItem;

use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;
use crate::utils::is_file_uri_in_dir;

pub fn on_did_open_text_document(
    lsp_state: &impl GlobalState,
    params: <DidOpenTextDocument as Notification>::Params,
) -> LSPRuntimeResult<()> {
    let DidOpenTextDocumentParams { text_document } = params;
    let TextDocumentItem { text, uri, .. } = text_document;

    if !is_file_uri_in_dir(lsp_state.root_dir(), &uri) {
        return Ok(());
    }

    lsp_state.document_opened(&uri, &text)
}

#[allow(clippy::unnecessary_wraps)]
pub fn on_did_close_text_document(
    lsp_state: &impl GlobalState,
    params: <DidCloseTextDocument as Notification>::Params,
) -> LSPRuntimeResult<()> {
    let uri = params.text_document.uri;

    if !is_file_uri_in_dir(lsp_state.root_dir(), &uri) {
        return Ok(());
    }

    lsp_state.document_closed(&uri)
}

pub fn on_did_change_text_document(
    lsp_state: &impl GlobalState,
    params: <DidChangeTextDocument as Notification>::Params,
) -> LSPRuntimeResult<()> {
    let DidChangeTextDocumentParams {
        content_changes,
        text_document,
    } = params;
    let uri = text_document.uri;

    if !is_file_uri_in_dir(lsp_state.root_dir(), &uri) {
        return Ok(());
    }

    // We do full text document syncing, so the new text will be in the first content change event.
    let content_change = content_changes
        .first()
        .expect("content_changes should always be non-empty");

    lsp_state.document_changed(&uri, &content_change.text)
}

#[allow(clippy::unnecessary_wraps)]
pub(crate) fn on_did_save_text_document(
    _lsp_state: &impl GlobalState,
    _params: <DidSaveTextDocument as Notification>::Params,
) -> LSPRuntimeResult<()> {
    Ok(())
}

#[allow(clippy::unnecessary_wraps)]
pub fn on_cancel(
    _lsp_state: &impl GlobalState,
    _params: <Cancel as Notification>::Params,
) -> LSPRuntimeResult<()> {
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::path::PathBuf;

use common::SourceLocationKey;
use common::Span;
use common::TextSource;
use dashmap::DashMap;
use docblock_syntax::parse_docblock;
use extract_graphql::JavaScriptSourceFeature;
use graphql_syntax::parse_executable_with_error_recovery;
use graphql_syntax::ExecutableDefinition;
use intern::string_key::StringKey;
use log::debug;
use lsp_types::Position;
use lsp_types::TextDocumentPositionParams;
use lsp_types::Url;
use relay_compiler::FileCategorizer;
use relay_compiler::FileGroup;
use relay_compiler::ProjectConfig;
use relay_docblock::parse_docblock_ast;
use relay_docblock::ParseOptions;

use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::Feature;

pub fn is_file_uri_in_dir(root_dir: PathBuf, file_uri: &Url) -> bool {
    let file_path_result = file_uri.to_file_path();

    match file_path_result {
        Ok(file_path) => file_path.starts_with(root_dir),
        Err(()) => false,
    }
}

pub fn extract_executable_definitions_from_text_document(
    text_document_uri: &Url,
    source_feature_cache: &DashMap<Url, Vec<JavaScriptSourceFeature>>,
) -> LSPRuntimeResult<Vec<ExecutableDefinition>> {
    let source_features = source_feature_cache
        .get(text_document_uri)
        // If the source isn't present in the source cache, then that means that
        // the source has no graphql documents.
        .ok_or(LSPRuntimeError::ExpectedError)?;

    let definitions = source_features
        .iter()
        .filter_map(|feature| match feature {
            JavaScriptSourceFeature::Docblock(_) => None,
            JavaScriptSourceFeature::GraphQL(graphql_source) => Some(graphql_source),
        })
        .map(|graphql_source| {
            let document = parse_executable_with_error_recovery(
                &graphql_source.text_source().text,
                SourceLocationKey::standalone(&text_document_uri.to_string()),
            )
            .item;

            document.definitions
        })
        .flatten()
        .collect::<Vec<ExecutableDefinition>>();

    Ok(definitions)
}

pub fn extract_project_name_from_url(
    file_categorizer: &FileCategorizer,
    url: &Url,
    root_dir: &PathBuf,
) -> LSPRuntimeResult<StringKey> {
    let absolute_file_path = url.to_file_path().map_err(|_| {
        LSPRuntimeError::UnexpectedError(format!("Unable to convert URL to file path: {:?}", url))
    })?;

    let file_path = absolute_file_path.strip_prefix(root_dir).map_err(|_e| {
        LSPRuntimeError::UnexpectedError(format!(
            "Failed to strip prefix {:?} from {:?}",
            root_dir, absolute_file_path
        ))
    })?;

    let project_name = if let FileGroup::Source { project_set } =
        file_categorizer.categorize(file_path).map_err(|_| {
            LSPRuntimeError::UnexpectedError(format!(
                "Unable to categorize the file correctly: {:?}",
                file_path
            ))
        })? {
        *project_set.first().ok_or_else(|| {
            LSPRuntimeError::UnexpectedError(format!(
                "Expected to find at least one project for {:?}",
                file_path
            ))
        })?
    } else {
        return Err(LSPRuntimeError::UnexpectedError(format!(
            "File path {:?} is not a source set",
            file_path
        )));
    };
    Ok(project_name)
}

/// Return a parsed executable document, or parsed Docblock IR for this LSP
/// request, only if the request occurs within a GraphQL document or Docblock.
pub fn extract_feature_from_text(
    project_config: &ProjectConfig,
    source_feature_cache: &DashMap<Url, Vec<JavaScriptSourceFeature>>,
    text_document_position: &TextDocumentPositionParams,
    index_offset: usize,
) -> LSPRuntimeResult<(Feature, Span)> {
    let uri = &text_document_position.text_document.uri;
    let position = text_document_position.position;

    let source_features = source_feature_cache
        .get(uri)
        .ok_or(LSPRuntimeError::ExpectedError)?;

    let (index, javascript_feature) = source_features
        .iter()
        .enumerate()
        .find(|(_, source_feature)| {
            let range = source_feature.text_source().to_range();
            position >= range.start && position <= range.end
        })
        .ok_or(LSPRuntimeError::ExpectedError)?;

    let source_location_key = SourceLocationKey::embedded(uri.as_ref(), index);

    match javascript_feature {
        JavaScriptSourceFeature::GraphQL(graphql_source) => {
            let document = parse_executable_with_error_recovery(
                &graphql_source.text_source().text,
                source_location_key,
            )
            .item;

            // Now we need to take the `Position` and map that to an offset relative
            // to this GraphQL document, as the `Span`s in the document are relative.
            debug!("Successfully parsed the definitions for a target GraphQL source");
            // Map the position to a zero-length span, relative to this GraphQL source.
            let position_span =
                position_to_span(&position, graphql_source.text_source(), index_offset)
                    .ok_or_else(|| {
                        LSPRuntimeError::UnexpectedError(
                            "Failed to map positions to spans".to_string(),
                        )
                    })?;

            // Now we need to walk the Document, tracking our path along the way, until
            // we find the position within the document. Note that the GraphQLSource will
            // already be updated *with the characters that triggered the completion request*
            // since the change event fires before completion.
            debug!("position_span: {:?}", position_span);

            Ok((Feature::GraphQLDocument(document), position_span))
        }
        JavaScriptSourceFeature::Docblock(docblock_source) => {
            let executable_definitions_in_file =
                extract_executable_definitions_from_text_document(uri, source_feature_cache)?;

            let text_source = &docblock_source.text_source();
            let text = &text_source.text;
            let docblock_ir = parse_docblock(text, source_location_key)
                .and_then(|ast| {
                    parse_docblock_ast(
                        &ast,
                        Some(&executable_definitions_in_file),
                        ParseOptions {
                            enable_output_type: &project_config
                                .feature_flags
                                .relay_resolver_enable_output_type,
                        },
                    )
                })
                .map_err(|_| {
                    LSPRuntimeError::UnexpectedError("Failed to parse docblock".to_string())
                })?
                .ok_or_else(|| {
                    LSPRuntimeError::UnexpectedError("No docblock IR found".to_string())
                })?;

            let position_span =
                position_to_offset(&position, index_offset, text_source.line_index, text)
                    .map(|offset| Span::new(offset, offset))
                    .ok_or_else(|| {
                        LSPRuntimeError::UnexpectedError(
                            "Failed to map positions to spans".to_string(),
                        )
                    })?;

            Ok((Feature::DocblockIr(docblock_ir), position_span))
        }
    }
}

/// Maps the LSP `Position` type back to a relative span, so we can find out which syntax node(s)
/// this request came from
fn position_to_span(position: &Position, source: &TextSource, index_offset: usize) -> Option<Span> {
    position_to_offset(position, index_offset, source.line_index, &source.text)
        .map(|offset| Span::new(offset, offset))
}

/// Find a character position in the GraphQL source text
/// from the Position (line, character) of the cursor in the IDE.
/// If the Position is outside of the source text, return None.
pub fn position_to_offset(
    position: &Position,
    index_offset: usize,
    line_index: usize,
    graphql_source_text: &str,
) -> Option<u32> {
    let mut index_of_first_character_of_current_line = 0;
    let mut line_index = line_index as u32;

    let mut chars = graphql_source_text.chars().enumerate().peekable();

    while let Some((index, chr)) = chars.next() {
        let is_newline = match chr {
            // Line terminators: https://www.ecma-international.org/ecma-262/#sec-line-terminators
            '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}' => {
                !matches!((chr, chars.peek()), ('\u{000D}', Some((_, '\u{000D}'))))
            }
            _ => false,
        };

        if is_newline {
            line_index += 1;
            // Add index_offset to account for different position index between hover and autocomplete
            index_of_first_character_of_current_line = index + index_offset;
        }

        if line_index == position.line {
            let start_offset =
                (index_of_first_character_of_current_line + position.character as usize) as u32;
            return Some(start_offset);
        }
    }
    None
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashSet;
use std::fmt;

use graphql_syntax::OperationKind;
use rand::seq::SliceRandom;
use relay_transforms::extract_module_name;

#[derive(Clone, Copy, Debug)]
pub enum DefinitionNameSuffix {
    Query,
    Mutation,
    Subscription,
}

impl From<&OperationKind> for DefinitionNameSuffix {
    fn from(kind: &OperationKind) -> Self {
        match kind {
            OperationKind::Query => DefinitionNameSuffix::Query,
            OperationKind::Subscription => DefinitionNameSuffix::Subscription,
            OperationKind::Mutation => DefinitionNameSuffix::Mutation,
        }
    }
}

impl fmt::Display for DefinitionNameSuffix {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

/// This function will create a default name suggestion for operation/fragment in a file.
/// Default name is {prefix}{Query|Mutation|Subscription},
/// where {prefix} is a cameCased base file stem, without extension and suffix (like .react.js, .jsx, etc..)
pub fn create_default_name(file_name: &str, suffix: DefinitionNameSuffix) -> Option<String> {
    let module_name = extract_module_name(file_name)?;
    if module_name.ends_with(&suffix.to_string()) {
        Some(module_name)
    } else {
        Some(format!("{}{}", module_name, suffix))
    }
}

/// This function will create a name suggestion for operation/fragment
/// in a file adding an incremental index.
/// Suggested name is {prefix}{index}{Query|Mutation|Subscription},
/// We will keep incrementing the index,
/// while the suggested name is not in the already used names in the file.
pub fn create_default_name_with_index(
    file_name: &str,
    suffix: DefinitionNameSuffix,
    used_names: &HashSet<String>,
) -> Option<String> {
    let module_name = extract_module_name(file_name)?;
    let mut index = 1;
    loop {
        let new_name = format!("{}{}{}", module_name, index, suffix);
        if used_names.contains(&new_name) {
            index += 1;
        } else {
            return Some(new_name);
        }
    }
}

/// If you already have a name for your fragment/operation,
/// it doesn't start/end with correct prefixes/suffixes -
/// this function will return a correctly wrapped name.
pub fn create_name_wrapper(
    original_name: &str,
    file_name: &str,
    suffix: DefinitionNameSuffix,
) -> Option<String> {
    let module_name = extract_module_name(file_name)?;
    let new_prefix = if original_name.starts_with(&module_name) {
        None
    } else {
        Some(module_name)
    };
    let new_suffix = if original_name.ends_with(&suffix.to_string()) {
        None
    } else {
        Some(suffix)
    };
    // if the `original_name` already have correct prefix/suffix, we don't need to return anything
    if new_prefix.is_none() && new_suffix.is_none() {
        None
    } else {
        let new_name = format!(
            "{}{}{}",
            match new_prefix {
                Some(value) => value,
                None => "".to_string(),
            },
            original_name,
            match new_suffix {
                Some(value) => value.to_string(),
                None => "".to_string(),
            },
        );
        Some(new_name)
    }
}

/// This function will create a very impactful name for your query/fragment
pub fn create_impactful_name(file_name: &str, suffix: DefinitionNameSuffix) -> Option<String> {
    let module_name = extract_module_name(file_name)?;

    // This will make your query/fragment more impactful
    let impact = create_impactful_part();

    Some(format!("{}{}{}", module_name, impact, suffix))
}

fn create_impactful_part() -> String {
    let adjectives = vec![
        "Redefined",
        "Awesome",
        "Formidable",
        "Remarkable",
        "Outstanding",
        "Fantastic",
        "Striking",
        "Noticeable",
        "Perceptible",
        "Fast",
        "Brisk",
        "Super",
        "Cool",
        "Impactful",
        "Meaningful",
        "DoubleQuick",
        "Prominent",
        "Swift",
        "Successful",
        "Impressive",
        "Egregious",
        "HighQuality",
        "Redefining",
        "Unique",
        "Impossible",
        "Robust",
        "Comprehensive",
        "Bold",
        "Useful",
        "Disruptive",
        "TopNotch",
        "WorldClass",
        "Exceptional",
    ];
    let nouns = vec![
        "Expectation",
        "Impact",
        "Awesomeness",
        "Feature",
        "Fix",
        "Improvement",
        "Implementation",
        "Project",
        "Success",
        "Profit",
        "Product",
        "Goal",
        "Effort",
        "Performance",
        "Result",
        "Solution",
    ];
    let adjective: &str = adjectives
        .choose(&mut rand::thread_rng())
        .unwrap_or(&adjectives[0]);
    let noun: &str = nouns.choose(&mut rand::thread_rng()).unwrap_or(&nouns[0]);

    format!("{}{}", adjective, noun)
}

#[cfg(not(windows))]
#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use super::create_default_name;
    use super::create_default_name_with_index;
    use super::create_name_wrapper;
    use super::DefinitionNameSuffix;

    #[test]
    fn test_create_default_name() {
        assert_eq!(
            create_default_name("/data/user/ReactFile.js", DefinitionNameSuffix::Query),
            Some("ReactFileQuery".to_string())
        );
        assert_eq!(
            create_default_name("/data/user/react-file.js", DefinitionNameSuffix::Query),
            Some("reactFileQuery".to_string())
        );
        assert_eq!(
            create_default_name("/data/user/My_react-file.js", DefinitionNameSuffix::Query),
            Some("MyReactFileQuery".to_string())
        );
        assert_eq!(
            create_default_name("/data/user/ReactFile.react.js", DefinitionNameSuffix::Query),
            Some("ReactFileQuery".to_string())
        );
        assert_eq!(
            create_default_name(
                "/data/user/ReactFile.android.js",
                DefinitionNameSuffix::Query
            ),
            Some("ReactFileAndroidQuery".to_string())
        );
        assert_eq!(
            create_default_name("/data/user/ReactFile.ios.js", DefinitionNameSuffix::Query),
            Some("ReactFileIosQuery".to_string())
        );
        assert_eq!(
            create_default_name("ReactFile", DefinitionNameSuffix::Query),
            Some("ReactFileQuery".to_string())
        );
        assert_eq!(
            create_default_name(
                "/data/users/project/EntityCreateMutation.js",
                DefinitionNameSuffix::Mutation
            ),
            Some("EntityCreateMutation".to_string())
        );
        assert_eq!(
            create_default_name("0001-----2-2-2", DefinitionNameSuffix::Query),
            Some("0001222Query".to_string())
        );
    }

    #[test]
    fn test_name_with_index1() {
        let mut used_names = HashSet::new();
        used_names.insert("ReactFileQuery".to_string());
        assert_eq!(
            create_default_name_with_index(
                "/data/user/ReactFile.js",
                DefinitionNameSuffix::Query,
                &used_names
            ),
            Some("ReactFile1Query".to_string())
        );
    }

    #[test]
    fn test_name_with_index_after_suffix() {
        let used_names = HashSet::new();
        assert_eq!(
            create_default_name_with_index(
                "/data/users/project/EntityCreateMutation.js",
                DefinitionNameSuffix::Mutation,
                &used_names
            ),
            Some("EntityCreateMutation1Mutation".to_string())
        );
    }

    #[test]
    fn test_name_with_index2() {
        let mut used_names = HashSet::new();
        used_names.insert("ReactFileQuery".to_string());
        used_names.insert("ReactFile1Query".to_string());
        assert_eq!(
            create_default_name_with_index(
                "/data/user/ReactFile.js",
                DefinitionNameSuffix::Query,
                &used_names
            ),
            Some("ReactFile2Query".to_string())
        );
    }

    #[test]
    fn test_create_name_wrapper() {
        assert_eq!(
            create_name_wrapper(
                "MyQuery",
                "/data/user/ReactFile.js",
                DefinitionNameSuffix::Query
            ),
            Some("ReactFileMyQuery".to_string())
        );
        assert_eq!(
            create_name_wrapper(
                "ReactFileMyQuery",
                "/data/user/ReactFile.js",
                DefinitionNameSuffix::Query
            ),
            None
        );
        assert_eq!(
            create_name_wrapper(
                "Test",
                "/data/user/ReactFile.js",
                DefinitionNameSuffix::Query
            ),
            Some("ReactFileTestQuery".to_string())
        );
    }
}

#[cfg(windows)]
#[cfg(test)]
mod tests {
    use super::create_default_name;
    use super::DefinitionNameSuffix;

    #[test]
    fn test_create_default_name() {
        assert_eq!(
            create_default_name("C:\\user\\ReactFile.js", DefinitionNameSuffix::Query),
            Some("ReactFileQuery".to_string())
        );
        assert_eq!(
            create_default_name(
                "\\\\?\\D:\\data\\user\\react-file.js",
                DefinitionNameSuffix::Query
            ),
            Some("reactFileQuery".to_string())
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

mod create_name_suggestion;

use std::collections::HashMap;
use std::collections::HashSet;

use common::Span;
use create_name_suggestion::create_default_name;
use create_name_suggestion::create_default_name_with_index;
use create_name_suggestion::create_impactful_name;
use create_name_suggestion::create_name_wrapper;
use create_name_suggestion::DefinitionNameSuffix;
use graphql_syntax::ExecutableDefinition;
use intern::Lookup;
use lsp_types::request::CodeActionRequest;
use lsp_types::request::Request;
use lsp_types::CodeAction;
use lsp_types::CodeActionOrCommand;
use lsp_types::Diagnostic;
use lsp_types::Position;
use lsp_types::Range;
use lsp_types::TextDocumentPositionParams;
use lsp_types::TextEdit;
use lsp_types::Url;
use lsp_types::WorkspaceEdit;
use resolution_path::IdentParent;
use resolution_path::IdentPath;
use resolution_path::OperationDefinitionPath;
use resolution_path::ResolutionPath;
use resolution_path::ResolvePosition;
use serde_json::Value;

use crate::lsp_runtime_error::LSPRuntimeError;
use crate::lsp_runtime_error::LSPRuntimeResult;
use crate::server::GlobalState;
use crate::utils::is_file_uri_in_dir;

pub(crate) fn on_code_action(
    state: &impl GlobalState,
    params: <CodeActionRequest as Request>::Params,
) -> LSPRuntimeResult<<CodeActionRequest as Request>::Result> {
    let uri = params.text_document.uri.clone();

    if !is_file_uri_in_dir(state.root_dir(), &uri) {
        return Err(LSPRuntimeError::ExpectedError);
    }

    if let Some(js_server) = state.get_js_language_sever() {
        if let Ok(result) = js_server.on_code_action(&params, state) {
            return Ok(result);
        }
    }

    if let Some(diagnostic) = state.get_diagnostic_for_range(&uri, params.range) {
        let code_actions = get_code_actions_from_diagnostics(&uri, diagnostic);
        if code_actions.is_some() {
            return Ok(code_actions);
        }
    }

    let definitions = state.resolve_executable_definitions(&params.text_document.uri)?;

    let text_document_position_params = TextDocumentPositionParams {
        text_document: params.text_document,
        position: params.range.start,
    };
    let (document, position_span) =
        state.extract_executable_document_from_text(&text_document_position_params, 1)?;

    let path = document.resolve((), position_span);

    let used_definition_names = get_definition_names(&definitions);
    let result = get_code_actions(path, used_definition_names, uri, params.range)
        .ok_or(LSPRuntimeError::ExpectedError)?;
    Ok(Some(result))
}

fn get_code_actions_from_diagnostics(
    url: &Url,
    diagnostic: Diagnostic,
) -> Option<Vec<CodeActionOrCommand>> {
    let code_actions = if let Some(Value::Array(data)) = &diagnostic.data {
        data.iter()
            .filter_map(|item| match item {
                Value::String(suggestion) => Some(create_code_action(
                    "Fix Error",
                    suggestion.to_string(),
                    url,
                    diagnostic.range,
                )),
                _ => None,
            })
            .collect::<_>()
    } else {
        vec![]
    };

    if !code_actions.is_empty() {
        Some(code_actions)
    } else {
        None
    }
}

struct FragmentAndOperationNames {
    operation_names: HashSet<String>,
    _fragment_names: HashSet<String>,
}

fn get_definition_names(definitions: &[ExecutableDefinition]) -> FragmentAndOperationNames {
    let mut operation_names = HashSet::new();
    let mut fragment_names = HashSet::new();
    for definition in definitions.iter() {
        match definition {
            ExecutableDefinition::Operation(operation) => {
                if let Some(name) = &operation.name {
                    operation_names.insert(name.value.lookup().to_string());
                }
            }
            ExecutableDefinition::Fragment(fragment) => {
                fragment_names.insert(fragment.name.value.lookup().to_string());
            }
        }
    }

    FragmentAndOperationNames {
        operation_names,
        _fragment_names: fragment_names,
    }
}

fn get_code_actions(
    path: ResolutionPath<'_>,
    used_definition_names: FragmentAndOperationNames,
    url: Url,
    range: Range,
) -> Option<Vec<CodeActionOrCommand>> {
    match path {
        ResolutionPath::Ident(IdentPath {
            inner: _,
            parent:
                IdentParent::OperationDefinitionName(OperationDefinitionPath {
                    inner: operation_definition,
                    parent: _,
                }),
        }) => {
            let suffix = if let Some((_, operation_kind)) = operation_definition.operation {
                DefinitionNameSuffix::from(&operation_kind)
            } else {
                return None;
            };

            let operation_name = if let Some(operation_name) = &operation_definition.name {
                operation_name
            } else {
                return None;
            };

            let code_action_range = get_code_action_range(range, operation_name.span);
            Some(create_code_actions(
                "Rename Operation",
                operation_name.value.lookup(),
                used_definition_names.operation_names,
                suffix,
                &url,
                code_action_range,
            ))
        }
        _ => None,
    }
}

fn create_code_actions(
    title: &str,
    original_name: &str,
    used_names: HashSet<String>,
    suffix: DefinitionNameSuffix,
    url: &Url,
    range: Range,
) -> Vec<CodeActionOrCommand> {
    let mut suggested_names = Vec::with_capacity(4);
    suggested_names.push(create_default_name(url.path(), suffix));
    suggested_names.push(create_default_name_with_index(
        url.path(),
        suffix,
        &used_names,
    ));
    suggested_names.push(create_name_wrapper(original_name, url.path(), suffix));
    suggested_names.push(create_impactful_name(url.path(), suffix));
    suggested_names
        .iter()
        .filter_map(|suggested_name| {
            if let Some(name) = suggested_name {
                if used_names.contains(name) {
                    return None;
                }

                Some(create_code_action(title, name.clone(), url, range))
            } else {
                None
            }
        })
        .collect::<Vec<_>>()
}

fn get_code_action_range(range: Range, span: Span) -> Range {
    Range {
        start: Position {
            line: range.start.line,
            character: (span.start - 1),
        },
        end: Position {
            line: range.start.line,
            character: (span.end - 1),
        },
    }
}

fn create_code_action(
    title: &str,
    new_name: String,
    url: &Url,
    range: Range,
) -> CodeActionOrCommand {
    let mut changes = HashMap::new();
    let title = format!("{}: '{}'", title, &new_name);
    let text_edit = TextEdit {
        range,
        new_text: new_name,
    };
    changes.insert(url.clone(), vec![text_edit]);

    CodeActionOrCommand::CodeAction(CodeAction {
        title,
        kind: Some(lsp_types::CodeActionKind::QUICKFIX),
        diagnostics: None,
        edit: Some(WorkspaceEdit {
            changes: Some(changes),
            document_changes: None,
            ..Default::default()
        }),
        command: None,
        is_preferred: Some(false),
        ..Default::default()
    })
}

#[cfg(test)]
mod tests {
    use lsp_types::CodeActionOrCommand;
    use lsp_types::Diagnostic;
    use lsp_types::Position;
    use lsp_types::Range;
    use lsp_types::Url;
    use serde_json::json;

    use crate::code_action::get_code_actions_from_diagnostics;

    #[test]
    fn test_get_code_actions_from_diagnostics() {
        let diagnostic = Diagnostic {
            range: Range {
                start: Position {
                    line: 0,
                    character: 0,
                },
                end: Position {
                    line: 0,
                    character: 0,
                },
            },
            message: "Error Message".to_string(),
            data: Some(json!(vec!["item1", "item2"])),
            ..Default::default()
        };
        let url = Url::parse("file://relay.js").unwrap();
        let code_actions = get_code_actions_from_diagnostics(&url, diagnostic);

        assert_eq!(
            code_actions
                .unwrap()
                .iter()
                .map(|item| {
                    match item {
                        CodeActionOrCommand::CodeAction(action) => action.title.clone(),
                        _ => panic!("unexpected case"),
                    }
                })
                .collect::<Vec<String>>(),
            vec![
                "Fix Error: 'item1'".to_string(),
                "Fix Error: 'item2'".to_string(),
            ]
        );
    }
}