Repository: microsoft/kiota (csharp)

Issue:
Overlapping paths cause invalid URLs
If a schema has partly overlapping paths which use different names for the parameters, then the URL can be invalid (in java generation at least).

This is the smallest example I can come up with:

```yaml
openapi: 3.0.3
servers:
  - url: 'https://example.com'
info:
  title: example
  version: 0.0.1
paths:
  '/path/{thingId}/abc/{second}':
    get:
      summary: abc
      operationId: abc
      parameters:
        - name: thingId
          in: path
          required: true
          schema:
            type: string
        - name: second
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
  '/path/{differentThingId}/def/{second}':
    get:
      summary: def
      operationId: def
      parameters:
        - name: differentThingId
          in: path
          required: true
          schema:
            type: string
        - name: second
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
```

### Steps to reproduce

Run command: `kiota generate --language java -n 'test'`
Version: `1.10.1` (docker container mcr.microsoft.com/openapi/kiota)

### Expected behaviour

`client.path().byPathId("<whatever>").abc().bySecond("<something>").toGetRequestInformation().urlTemplate == "{+baseurl}/path/{path-id}/abc/{second}"`

### Actual behaviour

`client.path().byPathId("<whatever>").abc().bySecond("<something>").toGetRequestInformation().urlTemplate == "{+baseurl}/path/{thingId}/abc/{second}"`
This causes a problem because the `byPathId` method stores the parameter with a name of `path%2Did` however the url template is looking for `thingId`. The URL ends up with that part of the path being blank.
No errors or warnings are logged by the generator.

### Workaround

Change path params to be named the same (i.e. in the above example change `thingId` and `differentThingId` to be named the same).
Of course this is only possible if you are in control of the schema in use - so in most cases this workaround won't work.

### Notes

I believe this is valid OpenAPI yaml - if not please feel free to close the ticket.

There are some similar issues logged in this repo which appear to be caused by ambiguity in the URLs, however there is no ambiguity in this case, as the third part of the path is different (`abc` vs `def`).

Pull request description:
After much thinking about it, I believe that this is the correct solution even if it might have performance implications.

Fixes #4151 
fixes #4085 .

## What's happening?

The `MergeIndexNodesAtSameLevel` deduplicates endpoints containing different path parameters forcing their name to become `{entity-id}`.

## Why?

[The specification](https://spec.openapis.org/oas/latest#path-templating-matching) seems to indicate that this pattern is invalid.
I tend to believe that this section is slightly under-specified, as it doesn't clearly state what should happen when the path parameter names are diverging, **and**, the matching of the path template is unique, e.g.:

```
  /pets/{petId}/fur
  /pets/{name}/hair
```
cc. Maybe @darrelmiller can shed light on this

## Current tradeoff

Under the assumption that the Path Templating Matching is invalid when the names of path segments are diverging the current optimization of merging the index nodes can be performed.
This will somehow also "help" as @baywet [mentioned](https://github.com/microsoft/kiota/issues/4085#issuecomment-1917300846):
> since this is a mistake often made in descriptions, [kiota attempts to merge the nodes](https://github.com/microsoft/kiota/blob/9f47a43bfa9b0987aa1ca871f0a25dd282135e26/src/Kiota.Builder/KiotaBuilder.cs#L544)

I believe that the current approach is misleading as we tend to agree that the path parameter names [should be preserved](https://github.com/microsoft/kiota/issues/4085#issuecomment-1917300846) and we got reports about collision issues with small and large API definitions.

## Proposed solution

Assuming that:
- we want the path parameter names to exactly match the definition
- we accept that any heuristic for deduplicating the path param names is going to generate bugs or, at least, unexpected results sooner or later(I don't know how to meaningfully distinguish `user/users-id`, `foo/users` and `template_owner/owner` at the same time)
- this might increase the number of operations that Kiota needs to do on a definition

Simply removing the merge function, we will be more closely mapping the original OpenAPI definition and avoiding this entire class of issues.

Patch:
diff --git a/it/config.json b/it/config.json
index 39ef0c14d5..e153f64c5a 100644
--- a/it/config.json
+++ b/it/config.json
@@ -73,6 +73,9 @@
   "./tests/Kiota.Builder.IntegrationTests/GeneratesUritemplateHints.yaml": {
     "MockServerITFolder": "query-params"
   },
+  "apisguru::github.com:api.github.com": {
+    "MockServerITFolder": "gh"
+  },
   "apisguru::notion.com": {
     "ExcludePatterns": [
       {
@@ -325,4 +328,4 @@
       }
     ]
   }
-}
+}
\ No newline at end of file
diff --git a/it/java/gh/pom.xml b/it/java/gh/pom.xml
new file mode 100644
index 0000000000..641d5bdc48
--- /dev/null
+++ b/it/java/gh/pom.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0"?>
+<project
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
+    xmlns="http://maven.apache.org/POM/4.0.0">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>io.kiota</groupId>
+    <artifactId>kiota-gh-api</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+
+    <properties>
+        <maven.compiler.release>11</maven.compiler.release>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+
+        <kiota-java.version>0.12.1</kiota-java.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.microsoft.kiota</groupId>
+            <artifactId>microsoft-kiota-abstractions</artifactId>
+            <version>${kiota-java.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.microsoft.kiota</groupId>
+            <artifactId>microsoft-kiota-serialization-json</artifactId>
+            <version>${kiota-java.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.microsoft.kiota</groupId>
+            <artifactId>microsoft-kiota-serialization-text</artifactId>
+            <version>${kiota-java.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.microsoft.kiota</groupId>
+            <artifactId>microsoft-kiota-serialization-form</artifactId>
+            <version>${kiota-java.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.microsoft.kiota</groupId>
+            <artifactId>microsoft-kiota-serialization-multipart</artifactId>
+            <version>${kiota-java.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.microsoft.kiota</groupId>
+            <artifactId>microsoft-kiota-http-okHttp</artifactId>
+            <version>${kiota-java.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.annotation</groupId>
+            <artifactId>jakarta.annotation-api</artifactId>
+            <version>2.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.9.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M9</version>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
index b531c7f77b..c88bce001d 100644
--- a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
+++ b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
@@ -3,6 +3,7 @@
 using System.Linq;
 using System.Text.RegularExpressions;
 using Kiota.Builder.Configuration;
+using Microsoft.Extensions.Logging;
 using Microsoft.OpenApi.MicrosoftExtensions;
 using Microsoft.OpenApi.Models;
 using Microsoft.OpenApi.Services;
@@ -79,7 +80,7 @@ public static IEnumerable<OpenApiParameter> GetPathParametersForCurrentSegment(t
                             .Where(static x => x.Value.PathItems.ContainsKey(Constants.DefaultOpenApiLabel))
                             .SelectMany(x => GetParametersForPathItem(x.Value.PathItems[Constants.DefaultOpenApiLabel], node.DeduplicatedSegment()))
                             .Distinct();
-        return Enumerable.Empty<OpenApiParameter>();
+        return [];
     }
     private const char PathNameSeparator = '\\';
     [GeneratedRegex(@"-?id\d?}?$", RegexOptions.Singleline | RegexOptions.IgnoreCase, 500)]
@@ -268,4 +269,101 @@ public static void AddDeduplicatedSegment(this OpenApiUrlTreeNode openApiUrlTree
         ArgumentException.ThrowIfNullOrEmpty(newName);
         openApiUrlTreeNode.AdditionalData.Add(DeduplicatedSegmentKey, [newName]);
     }
+    internal static void MergeIndexNodesAtSameLevel(this OpenApiUrlTreeNode node, ILogger logger)
+    {
+        var indexNodes = node.Children
+                        .Where(static x => x.Value.IsPathSegmentWithSingleSimpleParameter())
+                        .OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase)
+                        .ToArray();
+        if (indexNodes.Length > 1)
+        {
+            var indexNode = indexNodes[0];
+            node.Children.Remove(indexNode.Key);
+            var oldSegmentName = indexNode.Value.Segment.Trim('{', '}').CleanupSymbolName();
+            var segmentIndex = indexNode.Value.Path.Split('\\', StringSplitOptions.RemoveEmptyEntries).ToList().IndexOf(indexNode.Value.Segment);
+            var newSegmentParameterName = oldSegmentName.EndsWith("-id", StringComparison.OrdinalIgnoreCase) ? oldSegmentName : $"{{{oldSegmentName}-id}}";
+            indexNode.Value.Path = indexNode.Value.Path.Replace(indexNode.Key, newSegmentParameterName, StringComparison.OrdinalIgnoreCase);
+            indexNode.Value.AddDeduplicatedSegment(newSegmentParameterName);
+            node.Children.Add(newSegmentParameterName, indexNode.Value);
+            CopyNodeIntoOtherNode(indexNode.Value, indexNode.Value, indexNode.Key, newSegmentParameterName, logger);
+            foreach (var child in indexNodes.Except([indexNode]))
+            {
+                node.Children.Remove(child.Key);
+                CopyNodeIntoOtherNode(child.Value, indexNode.Value, child.Key, newSegmentParameterName, logger);
+            }
+            ReplaceParameterInPathForAllChildNodes(indexNode.Value, segmentIndex, newSegmentParameterName);
+        }
+
+        foreach (var child in node.Children.Values)
+            MergeIndexNodesAtSameLevel(child, logger);
+    }
+    private static void ReplaceParameterInPathForAllChildNodes(OpenApiUrlTreeNode node, int parameterIndex, string newParameterName)
+    {
+        if (parameterIndex < 0)
+            return;
+        foreach (var child in node.Children.Values)
+        {
+            var splatPath = child.Path.Split('\\', StringSplitOptions.RemoveEmptyEntries);
+            if (splatPath.Length > parameterIndex)
+            {
+                var oldName = splatPath[parameterIndex];
+                splatPath[parameterIndex] = newParameterName;
+                child.Path = "\\" + string.Join('\\', splatPath);
+                if (node.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem))
+                {
+                    foreach (var pathParameter in pathItem.Parameters
+                                                    .Union(pathItem.Operations.SelectMany(static x => x.Value.Parameters))
+                                                    .Where(x => x.In == ParameterLocation.Path && oldName.Equals(x.Name, StringComparison.Ordinal)))
+                    {
+                        pathParameter.Name = newParameterName;
+                    }
+                }
+            }
+            ReplaceParameterInPathForAllChildNodes(child, parameterIndex, newParameterName);
+        }
+    }
+    private static void CopyNodeIntoOtherNode(OpenApiUrlTreeNode source, OpenApiUrlTreeNode destination, string pathParameterNameToReplace, string pathParameterNameReplacement, ILogger logger)
+    {
+        foreach (var child in source.Children)
+        {
+            child.Value.Path = child.Value.Path.Replace(pathParameterNameToReplace, pathParameterNameReplacement, StringComparison.OrdinalIgnoreCase);
+            if (!destination.Children.TryAdd(child.Key, child.Value))
+                CopyNodeIntoOtherNode(child.Value, destination.Children[child.Key], pathParameterNameToReplace, pathParameterNameReplacement, logger);
+        }
+        pathParameterNameToReplace = pathParameterNameToReplace.Trim('{', '}');
+        pathParameterNameReplacement = pathParameterNameReplacement.Trim('{', '}');
+        foreach (var pathItem in source.PathItems)
+        {
+            foreach (var pathParameter in pathItem
+                                        .Value
+                                        .Parameters
+                                        .Where(x => x.In == ParameterLocation.Path && pathParameterNameToReplace.Equals(x.Name, StringComparison.Ordinal))
+                                        .Union(
+                                            pathItem
+                                                .Value
+                                                .Operations
+                                                .SelectMany(static x => x.Value.Parameters)
+                                                .Where(x => x.In == ParameterLocation.Path && pathParameterNameToReplace.Equals(x.Name, StringComparison.Ordinal))
+                                        ))
+            {
+                pathParameter.Name = pathParameterNameReplacement;
+            }
+            if (source != destination && !destination.PathItems.TryAdd(pathItem.Key, pathItem.Value))
+            {
+                var destinationPathItem = destination.PathItems[pathItem.Key];
+                foreach (var operation in pathItem.Value.Operations)
+                    if (!destinationPathItem.Operations.TryAdd(operation.Key, operation.Value))
+                    {
+                        logger.LogWarning("Duplicate operation {Operation} in path {Path}", operation.Key, pathItem.Key);
+                    }
+                foreach (var pathParameter in pathItem.Value.Parameters)
+                    destinationPathItem.Parameters.Add(pathParameter);
+                foreach (var extension in pathItem.Value.Extensions)
+                    if (!destinationPathItem.Extensions.TryAdd(extension.Key, extension.Value))
+                    {
+                        logger.LogWarning("Duplicate extension {Extension} in path {Path}", extension.Key, pathItem.Key);
+                    }
+            }
+        }
+    }
 }
diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs
index 0351366d1b..756afb3228 100644
--- a/src/Kiota.Builder/KiotaBuilder.cs
+++ b/src/Kiota.Builder/KiotaBuilder.cs
@@ -549,80 +549,11 @@ public OpenApiUrlTreeNode CreateUriSpace(OpenApiDocument doc)
         var stopwatch = new Stopwatch();
         stopwatch.Start();
         var node = OpenApiUrlTreeNode.Create(doc, Constants.DefaultOpenApiLabel);
-        MergeIndexNodesAtSameLevel(node);
+        node.MergeIndexNodesAtSameLevel(logger);
         stopwatch.Stop();
         logger.LogTrace("{Timestamp}ms: Created UriSpace tree", stopwatch.ElapsedMilliseconds);
         return node;
     }
-    private void MergeIndexNodesAtSameLevel(OpenApiUrlTreeNode node)
-    {
-        var indexNodes = node.Children
-                        .Where(static x => x.Value.IsPathSegmentWithSingleSimpleParameter())
-                        .OrderBy(static x => x.Key, StringComparer.OrdinalIgnoreCase)
-                        .ToArray();
-        if (indexNodes.Length > 1)
-        {
-            var indexNode = indexNodes[0];
-            node.Children.Remove(indexNode.Key);
-            var newSegmentParameterName = $"{{{node.Segment.CleanupSymbolName()}-id}}";
-            indexNode.Value.Path = indexNode.Value.Path.Replace(indexNode.Key, newSegmentParameterName, StringComparison.OrdinalIgnoreCase);
-            indexNode.Value.AddDeduplicatedSegment(newSegmentParameterName);
-            node.Children.Add(newSegmentParameterName, indexNode.Value);
-            CopyNodeIntoOtherNode(indexNode.Value, indexNode.Value, indexNode.Key, newSegmentParameterName);
-            foreach (var child in indexNodes.Except(new[] { indexNode }))
-            {
-                node.Children.Remove(child.Key);
-                CopyNodeIntoOtherNode(child.Value, indexNode.Value, child.Key, newSegmentParameterName);
-            }
-        }
-
-        foreach (var child in node.Children.Values)
-            MergeIndexNodesAtSameLevel(child);
-    }
-    private void CopyNodeIntoOtherNode(OpenApiUrlTreeNode source, OpenApiUrlTreeNode destination, string pathParameterNameToReplace, string pathParameterNameReplacement)
-    {
-        foreach (var child in source.Children)
-        {
-            child.Value.Path = child.Value.Path.Replace(pathParameterNameToReplace, pathParameterNameReplacement, StringComparison.OrdinalIgnoreCase);
-            if (!destination.Children.TryAdd(child.Key, child.Value))
-                CopyNodeIntoOtherNode(child.Value, destination.Children[child.Key], pathParameterNameToReplace, pathParameterNameReplacement);
-        }
-        pathParameterNameToReplace = pathParameterNameToReplace.Trim('{', '}');
-        pathParameterNameReplacement = pathParameterNameReplacement.Trim('{', '}');
-        foreach (var pathItem in source.PathItems)
-        {
-            foreach (var pathParameter in pathItem
-                                        .Value
-                                        .Parameters
-                                        .Where(x => x.In == ParameterLocation.Path && pathParameterNameToReplace.Equals(x.Name, StringComparison.Ordinal))
-                                        .Union(
-                                            pathItem
-                                                .Value
-                                                .Operations
-                                                .SelectMany(static x => x.Value.Parameters)
-                                                .Where(x => x.In == ParameterLocation.Path && pathParameterNameToReplace.Equals(x.Name, StringComparison.Ordinal))
-                                        ))
-            {
-                pathParameter.Name = pathParameterNameReplacement;
-            }
-            if (source != destination && !destination.PathItems.TryAdd(pathItem.Key, pathItem.Value))
-            {
-                var destinationPathItem = destination.PathItems[pathItem.Key];
-                foreach (var operation in pathItem.Value.Operations)
-                    if (!destinationPathItem.Operations.TryAdd(operation.Key, operation.Value))
-                    {
-                        logger.LogWarning("Duplicate operation {Operation} in path {Path}", operation.Key, pathItem.Key);
-                    }
-                foreach (var pathParameter in pathItem.Value.Parameters)
-                    destinationPathItem.Parameters.Add(pathParameter);
-                foreach (var extension in pathItem.Value.Extensions)
-                    if (!destinationPathItem.Extensions.TryAdd(extension.Key, extension.Value))
-                    {
-                        logger.LogWarning("Duplicate extension {Extension} in path {Path}", extension.Key, pathItem.Key);
-                    }
-            }
-        }
-    }
     private CodeNamespace? rootNamespace;
     private CodeNamespace? modelsNamespace;
     private string? modelNamespacePrefixToTrim;


Test patch:
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 53739c92f5..0d367a37e3 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -57,6 +57,7 @@ jobs:
           - "https://developers.pipedrive.com/docs/api/v1/openapi.yaml"
           - "apisguru::twilio.com:api"
           - "apisguru::docusign.net"
+          - "apisguru::github.com:api.github.com"
     steps:
       - uses: actions/checkout@v4
       - uses: actions/download-artifact@v4
diff --git a/it/java/gh/src/test/java/GHAPITest.java b/it/java/gh/src/test/java/GHAPITest.java
new file mode 100644
index 0000000000..cc9f50ec48
--- /dev/null
+++ b/it/java/gh/src/test/java/GHAPITest.java
@@ -0,0 +1,23 @@
+import apisdk.ApiClient;
+import com.microsoft.kiota.ApiException;
+import com.microsoft.kiota.authentication.AnonymousAuthenticationProvider;
+import com.microsoft.kiota.http.OkHttpRequestAdapter;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.TimeUnit;
+
+public class BasicAPITest {
+
+    @Test
+    void basicTest() throws Exception {
+        var adapter = new OkHttpRequestAdapter(new AnonymousAuthenticationProvider());
+        adapter.setBaseUrl("http://127.0.0.1:1080");
+        var client = new ApiClient(adapter);
+
+        client.repos().byOrgId("my-owner").byRepoId("my-repo").get();
+        client.repos().byOrgId("my-owner").byRepoId("my-repo").generate().post(null);
+    }
+
+}
diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs
index c97dd49ea1..0a7030661a 100644
--- a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs
+++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs
@@ -1,6 +1,8 @@
-﻿using System.Collections.Generic;
+﻿using System;
+using System.Collections.Generic;
 using System.Linq;
-
+using System.Net.Http;
+using Kiota.Builder.Configuration;
 using Kiota.Builder.Extensions;
 
 using Microsoft.OpenApi.Models;
@@ -9,7 +11,7 @@
 using Xunit;
 
 namespace Kiota.Builder.Tests.Extensions;
-public class OpenApiUrlTreeNodeExtensionsTests
+public sealed class OpenApiUrlTreeNodeExtensionsTests : IDisposable
 {
     [Fact]
     public void Defensive()
@@ -673,4 +675,335 @@ public void GetsClassNameWithSegmentsToSkipForClassNames()
         // validate that we get a valid class name
         Assert.Equal("json", responseClassName);
     }
+    [Fact]
+    public void SinglePathParametersAreDeduplicated()
+    {
+        var userSchema = new OpenApiSchema
+        {
+            Type = "object",
+            Properties = new Dictionary<string, OpenApiSchema> {
+                {
+                    "id", new OpenApiSchema {
+                        Type = "string"
+                    }
+                },
+                {
+                    "displayName", new OpenApiSchema {
+                        Type = "string"
+                    }
+                }
+            },
+            Reference = new OpenApiReference
+            {
+                Id = "#/components/schemas/microsoft.graph.user"
+            },
+            UnresolvedReference = false
+        };
+        var document = new OpenApiDocument
+        {
+            Paths = new OpenApiPaths
+            {
+                ["users/{foo}/careerAdvisor/{id}"] = new OpenApiPathItem
+                {
+                    Parameters = {
+                        new OpenApiParameter {
+                            Name = "foo",
+                            In = ParameterLocation.Path,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                    },
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = userSchema
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                ["users/{id}/careerAdvisor"] = new OpenApiPathItem
+                {
+                    Parameters = {
+                        new OpenApiParameter {
+                            Name = "id",
+                            In = ParameterLocation.Path,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                    },
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = userSchema
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                ["users/{user-id}/manager"] = new OpenApiPathItem
+                {
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Parameters = {
+                                new OpenApiParameter {
+                                    Name = "user-id",
+                                    In = ParameterLocation.Path,
+                                    Required = true,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                            },
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = userSchema
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+            },
+            Components = new OpenApiComponents
+            {
+                Schemas = new Dictionary<string, OpenApiSchema> {
+                    {
+                        "microsoft.graph.user", userSchema
+                    }
+                }
+            }
+        };
+        var mockLogger = new CountLogger<KiotaBuilder>();
+        var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient);
+        var node = builder.CreateUriSpace(document);
+        node.MergeIndexNodesAtSameLevel(mockLogger);
+        var usersCollectionIndexNode = GetChildNodeByPath(node, "users/{foo-id}");
+        Assert.NotNull(usersCollectionIndexNode);
+        Assert.Equal("{+baseurl}/users/{foo%2Did}", usersCollectionIndexNode.GetUrlTemplate());
+
+        var managerNode = GetChildNodeByPath(node, "users/{foo-id}/manager");
+        Assert.NotNull(managerNode);
+        Assert.Equal("{+baseurl}/users/{foo%2Did}/manager", managerNode.GetUrlTemplate());
+
+        var careerAdvisorNode = GetChildNodeByPath(node, "users/{foo-id}/careerAdvisor");
+        Assert.NotNull(careerAdvisorNode);
+        Assert.Equal("{+baseurl}/users/{foo%2Did}/careerAdvisor", careerAdvisorNode.GetUrlTemplate());
+
+        var careerAdvisorIndexNode = GetChildNodeByPath(node, "users/{foo-id}/careerAdvisor/{id}");
+        Assert.NotNull(careerAdvisorIndexNode);
+        Assert.Equal("{+baseurl}/users/{foo%2Did}/careerAdvisor/{id}", careerAdvisorIndexNode.GetUrlTemplate());
+        var pathItem = careerAdvisorIndexNode.PathItems[Constants.DefaultOpenApiLabel];
+        Assert.NotNull(pathItem);
+        var parameter = pathItem.Parameters.FirstOrDefault(static p => p.Name == "foo-id");
+        Assert.NotNull(parameter);
+    }
+    [Fact]
+    public void SinglePathParametersAreDeduplicatedAndOrderIsRespected()
+    {
+        var ownerSchema = new OpenApiSchema
+        {
+            Type = "object",
+            Properties = new Dictionary<string, OpenApiSchema> {
+                {
+                    "id", new OpenApiSchema {
+                        Type = "string"
+                    }
+                }
+            },
+            Reference = new OpenApiReference
+            {
+                Id = "#/components/schemas/owner"
+            },
+            UnresolvedReference = false
+        };
+        var repoSchema = new OpenApiSchema
+        {
+            Type = "object",
+            Properties = new Dictionary<string, OpenApiSchema> {
+                {
+                    "id", new OpenApiSchema {
+                        Type = "string"
+                    }
+                }
+            },
+            Reference = new OpenApiReference
+            {
+                Id = "#/components/schemas/repo"
+            },
+            UnresolvedReference = false
+        };
+        var document = new OpenApiDocument
+        {
+            Paths = new OpenApiPaths
+            {
+                ["/repos/{owner}/{repo}"] = new OpenApiPathItem
+                {
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = repoSchema
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                ["/repos/{template_owner}/{template_repo}/generate"] = new OpenApiPathItem
+                {
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = repoSchema
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            Components = new OpenApiComponents
+            {
+                Schemas = new Dictionary<string, OpenApiSchema> {
+                    {"owner", ownerSchema},
+                    {"repo", repoSchema}
+                }
+            }
+        };
+        var mockLogger = new CountLogger<KiotaBuilder>();
+        var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "GitHub", ApiRootUrl = "https://localhost" }, _httpClient);
+        var node = builder.CreateUriSpace(document);
+        node.MergeIndexNodesAtSameLevel(mockLogger);
+
+        // Expected
+        var resultNode = GetChildNodeByPath(node, "repos/{owner-id}/{repo-id}/generate");
+        Assert.NotNull(resultNode);
+        Assert.Equal("\\repos\\{owner-id}\\{repo-id}\\generate", resultNode.Path);
+        Assert.Equal("{+baseurl}/repos/{owner%2Did}/{repo%2Did}/generate", resultNode.GetUrlTemplate());
+    }
+    [Fact]
+    public void repro4085()
+    {
+        var document = new OpenApiDocument
+        {
+            Paths = new OpenApiPaths
+            {
+                ["/path/{thingId}/abc/{second}"] = new OpenApiPathItem
+                {
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = new OpenApiSchema {
+                                                Type = "string"
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                ["/path/{differentThingId}/def/{second}"] = new OpenApiPathItem
+                {
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = new OpenApiSchema {
+                                                Type = "string"
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        };
+        var mockLogger = new CountLogger<KiotaBuilder>();
+        var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "GitHub", ApiRootUrl = "https://localhost" }, _httpClient);
+        var node = builder.CreateUriSpace(document);
+        node.MergeIndexNodesAtSameLevel(mockLogger);
+
+        // Expected
+        var resultNode = GetChildNodeByPath(node, "path");
+        Assert.NotNull(resultNode);
+        Assert.Equal("\\path", resultNode.Path);
+
+        Assert.Null(GetChildNodeByPath(resultNode, "{thingId}"));
+        Assert.Null(GetChildNodeByPath(resultNode, "{differentThingId}"));
+
+        var differentThingId = GetChildNodeByPath(resultNode, "{differentThingId-id}");
+        Assert.Equal("\\path\\{differentThingId-id}", differentThingId.Path);
+        Assert.Equal("{+baseurl}/path/{differentThingId%2Did}", differentThingId.GetUrlTemplate());
+    }
+    private static OpenApiUrlTreeNode GetChildNodeByPath(OpenApiUrlTreeNode node, string path)
+    {
+        var pathSegments = path.Split('/');
+        if (pathSegments.Length == 0)
+            return null;
+        if (pathSegments.Length == 1 && node.Children.TryGetValue(pathSegments[0], out var result))
+            return result;
+        if (node.Children.TryGetValue(pathSegments[0], out var currentNode))
+            return GetChildNodeByPath(currentNode, string.Join('/', pathSegments.Skip(1)));
+        return null;
+    }
+    public void Dispose()
+    {
+        _httpClient.Dispose();
+        GC.SuppressFinalize(this);
+    }
+    private static readonly HttpClient _httpClient = new();
 }
diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
index cc0f7850af..473b2775b4 100644
--- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
+++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
@@ -6472,126 +6472,6 @@ await File.WriteAllTextAsync(tempFilePath, @$"openapi: 3.0.1
         Assert.NotNull(inlineType);
     }
     [Fact]
-    public void SinglePathParametersAreDeduplicated()
-    {
-        var userSchema = new OpenApiSchema
-        {
-            Type = "object",
-            Properties = new Dictionary<string, OpenApiSchema> {
-                {
-                    "id", new OpenApiSchema {
-                        Type = "string"
-                    }
-                },
-                {
-                    "displayName", new OpenApiSchema {
-                        Type = "string"
-                    }
-                }
-            },
-            Reference = new OpenApiReference
-            {
-                Id = "#/components/schemas/microsoft.graph.user"
-            },
-            UnresolvedReference = false
-        };
-        var document = new OpenApiDocument
-        {
-            Paths = new OpenApiPaths
-            {
-                ["users/{foo}/careerAdvisor/{id}"] = new OpenApiPathItem
-                {
-                    Operations = {
-                        [OperationType.Get] = new OpenApiOperation
-                        {
-                            Responses = new OpenApiResponses {
-                                ["200"] = new OpenApiResponse
-                                {
-                                    Content = {
-                                        ["application/json"] = new OpenApiMediaType
-                                        {
-                                            Schema = userSchema
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                },
-                ["users/{id}/careerAdvisor"] = new OpenApiPathItem
-                {
-                    Operations = {
-                        [OperationType.Get] = new OpenApiOperation
-                        {
-                            Responses = new OpenApiResponses {
-                                ["200"] = new OpenApiResponse
-                                {
-                                    Content = {
-                                        ["application/json"] = new OpenApiMediaType
-                                        {
-                                            Schema = userSchema
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                },
-                ["users/{user-id}/manager"] = new OpenApiPathItem
-                {
-                    Operations = {
-                        [OperationType.Get] = new OpenApiOperation
-                        {
-                            Responses = new OpenApiResponses {
-                                ["200"] = new OpenApiResponse
-                                {
-                                    Content = {
-                                        ["application/json"] = new OpenApiMediaType
-                                        {
-                                            Schema = userSchema
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                },
-            },
-            Components = new OpenApiComponents
-            {
-                Schemas = new Dictionary<string, OpenApiSchema> {
-                    {
-                        "microsoft.graph.user", userSchema
-                    }
-                }
-            }
-        };
-        var mockLogger = new CountLogger<KiotaBuilder>();
-        var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient);
-        var node = builder.CreateUriSpace(document);
-        var codeModel = builder.CreateSourceModel(node);
-        var usersRB = codeModel.FindNamespaceByName("ApiSdk.users").FindChildByName<CodeClass>("UsersRequestBuilder", false);
-        Assert.NotNull(usersRB);
-        var usersIndexer = usersRB.Indexer;
-        Assert.NotNull(usersIndexer);
-        Assert.Equal("users%2Did", usersIndexer.IndexParameter.SerializationName);
-        var managerRB = codeModel.FindNamespaceByName("ApiSdk.users.item.manager").FindChildByName<CodeClass>("ManagerRequestBuilder", false);
-        Assert.NotNull(managerRB);
-        var managerUrlTemplate = managerRB.FindChildByName<CodeProperty>("UrlTemplate", false);
-        Assert.NotNull(managerUrlTemplate);
-        Assert.Equal("{+baseurl}/users/{users%2Did}/manager", managerUrlTemplate.DefaultValue.Trim('"'));
-        var careerAdvisorRB = codeModel.FindNamespaceByName("ApiSdk.users.item.careerAdvisor").FindChildByName<CodeClass>("CareerAdvisorRequestBuilder", false);
-        Assert.NotNull(careerAdvisorRB);
-        var careerAdvisorUrlTemplate = careerAdvisorRB.FindChildByName<CodeProperty>("UrlTemplate", false);
-        Assert.NotNull(careerAdvisorUrlTemplate);
-        Assert.Equal("{+baseurl}/users/{users%2Did}/careerAdvisor", careerAdvisorUrlTemplate.DefaultValue.Trim('"'));
-        var careerAdvisorItemRB = codeModel.FindNamespaceByName("ApiSdk.users.item.careerAdvisor.item").FindChildByName<CodeClass>("CareerAdvisorItemRequestBuilder", false);
-        Assert.NotNull(careerAdvisorItemRB);
-        var careerAdvisorItemUrlTemplate = careerAdvisorItemRB.FindChildByName<CodeProperty>("UrlTemplate", false);
-        Assert.NotNull(careerAdvisorItemUrlTemplate);
-        Assert.Equal("{+baseurl}/users/{users%2Did}/careerAdvisor/{id}", careerAdvisorItemUrlTemplate.DefaultValue.Trim('"'));
-    }
-    [Fact]
     public void AddReservedPathParameterSymbol()
     {
         var userSchema = new OpenApiSchema